From 1df8ad63de869eb073769146b3504b5609df4949 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Tue, 25 Oct 2022 20:48:57 +0400 Subject: [PATCH] Add more ways to disable telemetry (#1311) --- .golangci-new.yml | 11 +- .golangci.yml | 8 - Taskfile.yml | 16 +- cmd/ferretdb/main.go | 15 +- integration/setup/setup.go | 3 +- integration/setup/setup_compat.go | 3 +- internal/handlers/pg/pgdb/pgdb_test.go | 5 +- .../handlers/tigris/tigrisdb/tigrisdb_test.go | 3 +- internal/util/debug/debug.go | 1 + internal/util/hex/hex.go | 1 + internal/util/telemetry/reporter.go | 224 +++++++++++++++++ internal/util/telemetry/telemetry.go | 236 ++++++------------ internal/util/telemetry/telemetry_test.go | 78 ++++++ internal/util/testutil/testutil.go | 13 + 14 files changed, 421 insertions(+), 196 deletions(-) create mode 100644 internal/util/telemetry/reporter.go create mode 100644 internal/util/telemetry/telemetry_test.go diff --git a/.golangci-new.yml b/.golangci-new.yml index 7b1187d0e566..bbc5d3c476f0 100644 --- a/.golangci-new.yml +++ b/.golangci-new.yml @@ -10,7 +10,15 @@ linters-settings: errorf: false asserts: true comparison: true - + revive: + ignore-generated-header: true + severity: warning + # TODO enable-all-rules: true + rules: + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md + - name: exported + arguments: [checkPrivateReceivers] + - name: package-comments staticcheck: checks: ["all"] wsl: @@ -48,7 +56,6 @@ linters: - lll - misspell - nolintlint - - revive - unused - whitespace diff --git a/.golangci.yml b/.golangci.yml index 0c56fd571d14..b7d871fca246 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -86,13 +86,6 @@ linters-settings: allow-no-explanation: [] require-explanation: true require-specific: true - revive: - ignore-generated-header: true - severity: warning - # TODO enable-all-rules: true - rules: - - name: exported - # TODO arguments: [checkPrivateReceivers] staticcheck: checks: - all @@ -122,7 +115,6 @@ linters: - lll - misspell - nolintlint - - revive - staticcheck - unused - whitespace diff --git a/Taskfile.yml b/Taskfile.yml index fbe0d7264694..58b5729ff975 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -234,23 +234,31 @@ tasks: desc: "Run FerretDB" deps: [build-testcover] cmds: - - bin/ferretdb-testcover{{exeExt}} -test.coverprofile=cover.txt -- --test-records-dir=records --mode=diff-normal --listen-addr=:27017 + - > + bin/ferretdb-testcover{{exeExt}} -test.coverprofile=cover.txt -- --test-records-dir=records + --mode=diff-normal --listen-addr=:27017 run-tigris: deps: [build-testcover] cmds: - - bin/ferretdb-testcover{{exeExt}} -test.coverprofile=cover.txt -- --test-records-dir=records --mode=diff-normal --listen-addr=:27017 --handler=tigris + - > + bin/ferretdb-testcover{{exeExt}} -test.coverprofile=cover.txt -- --test-records-dir=records + --mode=diff-normal --listen-addr=:27017 --handler=tigris run-cockroach: deps: [build-testcover] cmds: - - bin/ferretdb-testcover{{exeExt}} -test.coverprofile=cover.txt -- --test-records-dir=records --mode=diff-normal --listen-addr=:27017 --postgresql-url=postgres://cockroach@127.0.0.1:26257/ferretdb + - > + bin/ferretdb-testcover{{exeExt}} -test.coverprofile=cover.txt -- --test-records-dir=records + --mode=diff-normal --listen-addr=:27017 --postgresql-url=postgres://cockroach@127.0.0.1:26257/ferretdb run-proxy: desc: "Run FerretDB in diff-proxy mode" deps: [build-testcover] cmds: - - bin/ferretdb-testcover{{exeExt}} -test.coverprofile=cover.txt -- --test-records-dir=records --mode=diff-proxy --listen-addr=:27017 + - > + bin/ferretdb-testcover{{exeExt}} -test.coverprofile=cover.txt -- --test-records-dir=records + --mode=diff-proxy --listen-addr=:27017 lint: desc: "Run linters" diff --git a/cmd/ferretdb/main.go b/cmd/ferretdb/main.go index 768fbcf79796..a798cd2505d4 100644 --- a/cmd/ferretdb/main.go +++ b/cmd/ferretdb/main.go @@ -26,7 +26,6 @@ import ( "sync" "time" - "github.com/AlekSi/pointer" "github.com/alecthomas/kong" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/expfmt" @@ -62,8 +61,7 @@ var cli struct { MetricsUUID bool `default:"false" help:"Add instance UUID to all metrics."` - // TODO switch to string for 3 states - Telemetry bool `default:"false" help:"Enable basic telemetry. See https://beacon.ferretdb.io."` + Telemetry telemetry.Flag `default:"" help:"Enable or disable basic telemetry. See https://beacon.ferretdb.io."` Handler string `default:"pg" help:"${help_handler}"` @@ -199,13 +197,10 @@ func setupLogger(stateProvider *state.Provider) *zap.Logger { } // runTelemetryReporter runs telemetry reporter until ctx is canceled. -func runTelemetryReporter(ctx context.Context, enabled bool, opts *telemetry.NewReporterOpts) { - // TODO probably move out of this function - opts.P.Update(func(s *state.State) { s.Telemetry = pointer.ToBool(enabled) }) - +func runTelemetryReporter(ctx context.Context, opts *telemetry.NewReporterOpts) { r, err := telemetry.NewReporter(opts) if err != nil { - opts.L.Fatal("Failed to create telemetry reporter.", zap.Error(err)) + opts.L.Sugar().Fatalf("Failed to create telemetry reporter: %s.", err) } r.Run(ctx) @@ -253,9 +248,11 @@ func run() { defer wg.Done() runTelemetryReporter( ctx, - cli.Telemetry, &telemetry.NewReporterOpts{ URL: cli.Test.Telemetry.URL, + F: &cli.Telemetry, + DNT: os.Getenv("DO_NOT_TRACK"), + ExecName: os.Args[0], P: stateProvider, L: logger.Named("telemetry"), UndecidedDelay: cli.Test.Telemetry.UndecidedDelay, diff --git a/integration/setup/setup.go b/integration/setup/setup.go index 48301e31ca55..b423a0203fbf 100644 --- a/integration/setup/setup.go +++ b/integration/setup/setup.go @@ -23,7 +23,6 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.uber.org/zap" - "go.uber.org/zap/zaptest" "golang.org/x/exp/slices" "github.com/FerretDB/FerretDB/integration/shareddata" @@ -68,7 +67,7 @@ func SetupWithOpts(tb testing.TB, opts *SetupOpts) *SetupResult { if *debugSetupF { level = zap.NewAtomicLevelAt(zap.DebugLevel) } - logger := zaptest.NewLogger(tb, zaptest.Level(level), zaptest.WrapOptions(zap.AddCaller(), zap.Development())) + logger := testutil.Logger(tb, level) port := *targetPortF if port == 0 { diff --git a/integration/setup/setup_compat.go b/integration/setup/setup_compat.go index b3288967d07f..f28e6f2811ff 100644 --- a/integration/setup/setup_compat.go +++ b/integration/setup/setup_compat.go @@ -25,7 +25,6 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.uber.org/zap" - "go.uber.org/zap/zaptest" "golang.org/x/exp/slices" "github.com/FerretDB/FerretDB/integration/shareddata" @@ -92,7 +91,7 @@ func SetupCompatWithOpts(tb testing.TB, opts *SetupCompatOpts) *SetupCompatResul if *debugSetupF { level = zap.NewAtomicLevelAt(zap.DebugLevel) } - logger := zaptest.NewLogger(tb, zaptest.Level(level), zaptest.WrapOptions(zap.AddCaller(), zap.Development())) + logger := testutil.Logger(tb, level) targetPort := *targetPortF if targetPort == 0 { diff --git a/internal/handlers/pg/pgdb/pgdb_test.go b/internal/handlers/pg/pgdb/pgdb_test.go index 3df595ce8a5d..a6a4a774b99d 100644 --- a/internal/handlers/pg/pgdb/pgdb_test.go +++ b/internal/handlers/pg/pgdb/pgdb_test.go @@ -22,7 +22,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" - "go.uber.org/zap/zaptest" "github.com/FerretDB/FerretDB/internal/util/testutil" ) @@ -31,8 +30,8 @@ import ( func getPool(ctx context.Context, tb testing.TB) *Pool { tb.Helper() - l := zaptest.NewLogger(tb, zaptest.WrapOptions(zap.AddCaller(), zap.Development())) - pool, err := NewPool(ctx, testutil.PostgreSQLURL(tb, nil), l, false) + logger := testutil.Logger(tb, zap.NewAtomicLevelAt(zap.DebugLevel)) + pool, err := NewPool(ctx, testutil.PostgreSQLURL(tb, nil), logger, false) require.NoError(tb, err) tb.Cleanup(pool.Close) diff --git a/internal/handlers/tigris/tigrisdb/tigrisdb_test.go b/internal/handlers/tigris/tigrisdb/tigrisdb_test.go index 1358336c810f..2e2ae6218540 100644 --- a/internal/handlers/tigris/tigrisdb/tigrisdb_test.go +++ b/internal/handlers/tigris/tigrisdb/tigrisdb_test.go @@ -24,7 +24,6 @@ import ( "github.com/tigrisdata/tigris-client-go/config" "github.com/tigrisdata/tigris-client-go/driver" "go.uber.org/zap" - "go.uber.org/zap/zaptest" "github.com/FerretDB/FerretDB/internal/util/testutil" ) @@ -37,7 +36,7 @@ func TestCreateCollectionIfNotExist(t *testing.T) { URL: testutil.TigrisURL(t), } - logger := zaptest.NewLogger(t, zaptest.WrapOptions(zap.AddCaller(), zap.Development())) + logger := testutil.Logger(t, zap.NewAtomicLevelAt(zap.DebugLevel)) tdb, err := New(cfg, logger) require.NoError(t, err) diff --git a/internal/util/debug/debug.go b/internal/util/debug/debug.go index daa25dbb139c..edb538382bf4 100644 --- a/internal/util/debug/debug.go +++ b/internal/util/debug/debug.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package debug provides debug facilities. package debug import ( diff --git a/internal/util/hex/hex.go b/internal/util/hex/hex.go index ec5175d8daf2..7780514d23b1 100644 --- a/internal/util/hex/hex.go +++ b/internal/util/hex/hex.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package hex provides helpers for working with hex dumps. package hex import ( diff --git a/internal/util/telemetry/reporter.go b/internal/util/telemetry/reporter.go new file mode 100644 index 000000000000..c5cecd809284 --- /dev/null +++ b/internal/util/telemetry/reporter.go @@ -0,0 +1,224 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package telemetry + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "runtime" + "time" + + "github.com/AlekSi/pointer" + "go.uber.org/zap" + + "github.com/FerretDB/FerretDB/internal/util/state" + "github.com/FerretDB/FerretDB/internal/util/version" +) + +// request represents telemetry request. +type request struct { + Version string `json:"version"` + Commit string `json:"commit"` + Branch string `json:"branch"` + Dirty bool `json:"dirty"` + Debug bool `json:"debug"` + OS string `json:"os"` + Arch string `json:"arch"` + + UUID string `json:"uuid"` + Uptime time.Duration `json:"uptime"` +} + +// response represents telemetry response. +type response struct { + LatestVersion string `json:"latest_version"` +} + +// Reporter sends telemetry reports if telemetry is enabled. +type Reporter struct { + *NewReporterOpts + c *http.Client +} + +// NewReporterOpts represents reporter options. +type NewReporterOpts struct { + URL string + F *Flag + DNT string + ExecName string + P *state.Provider + L *zap.Logger + UndecidedDelay time.Duration + ReportInterval time.Duration + ReportTimeout time.Duration +} + +// NewReporter creates a new reporter. +func NewReporter(opts *NewReporterOpts) (*Reporter, error) { + t, err := initialState(opts.F, opts.DNT, opts.ExecName, opts.P.Get().Telemetry, opts.L) + if err != nil { + return nil, err + } + + if err = opts.P.Update(func(s *state.State) { s.Telemetry = t }); err != nil { + return nil, err + } + + return &Reporter{ + NewReporterOpts: opts, + c: http.DefaultClient, + }, nil +} + +// Run runs reporter until context is canceled. +func (r *Reporter) Run(ctx context.Context) { + r.L.Debug("Reporter started.") + defer r.L.Debug("Reporter stopped.") + + ch := r.P.Subscribe() + + r.firstReportDelay(ctx, ch) + + for ctx.Err() == nil { + r.report(ctx) + + delayCtx, delayCancel := context.WithTimeout(ctx, r.ReportInterval) + <-delayCtx.Done() + delayCancel() + } + + // do one last report before exiting if telemetry is explicitly enabled + if pointer.GetBool(r.P.Get().Telemetry) { + r.report(context.Background()) + } +} + +// firstReportDelay waits until telemetry reporting state is decided, +// main context is cancelled, or timeout is reached. +func (r *Reporter) firstReportDelay(ctx context.Context, ch <-chan struct{}) { + if r.P.Get().Telemetry != nil { + return + } + + msg := fmt.Sprintf( + "Telemetry state undecided, waiting %s before the first report. "+ + "Read more about FerretDB telemetry at https://beacon.ferretdb.io", + r.UndecidedDelay, + ) + r.L.Info(msg) + + delayCtx, delayCancel := context.WithTimeout(ctx, r.UndecidedDelay) + defer delayCancel() + + for { + select { + case <-delayCtx.Done(): + return + case <-ch: + if r.P.Get().Telemetry != nil { + return + } + } + } +} + +// makeRequest creates a new telemetry request. +func makeRequest(s *state.State) *request { + v := version.Get() + + return &request{ + Version: v.Version, + Commit: v.Commit, + Branch: v.Branch, + Dirty: v.Dirty, + Debug: v.Debug, + OS: runtime.GOOS, + Arch: runtime.GOARCH, + + UUID: s.UUID, + Uptime: time.Since(s.Start), + } +} + +// report sends telemetry report unless telemetry is disabled. +func (r *Reporter) report(ctx context.Context) { + s := r.P.Get() + if s.Telemetry != nil && !*s.Telemetry { + r.L.Debug("Telemetry is disabled, skipping reporting.") + return + } + + request := makeRequest(s) + r.L.Info("Reporting telemetry.", zap.Reflect("data", request)) + + b, err := json.Marshal(request) + if err != nil { + r.L.Error("Failed to marshal telemetry request.", zap.Error(err)) + return + } + + reqCtx, reqCancel := context.WithTimeout(ctx, r.ReportTimeout) + defer reqCancel() + + req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, r.URL, bytes.NewReader(b)) + if err != nil { + r.L.Error("Failed to create telemetry request.", zap.Error(err)) + return + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + res, err := r.c.Do(req) + if err != nil { + r.L.Debug("Failed to send telemetry request.", zap.Error(err)) + return + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + r.L.Debug("Failed to send telemetry request.", zap.Int("status", res.StatusCode)) + return + } + + var response response + if err = json.NewDecoder(res.Body).Decode(&response); err != nil { + r.L.Debug("Failed to read telemetry response.", zap.Error(err)) + return + } + + if response.LatestVersion == "" { + r.L.Debug("No latest version in telemetry response.") + return + } + + if response.LatestVersion == s.LatestVersion { + r.L.Debug("Latest version is up to date.") + return + } + + r.L.Info( + "New version available.", + zap.String("current_version", request.Version), zap.String("latest_version", response.LatestVersion), + ) + + err = r.P.Update(func(s *state.State) { s.LatestVersion = response.LatestVersion }) + if err != nil { + r.L.Error("Failed to update state with latest version.", zap.Error(err)) + return + } +} diff --git a/internal/util/telemetry/telemetry.go b/internal/util/telemetry/telemetry.go index 6f3a5a249b2b..3a2e6355999f 100644 --- a/internal/util/telemetry/telemetry.go +++ b/internal/util/telemetry/telemetry.go @@ -12,201 +12,109 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package telemetry provides basic telemetry facilities. package telemetry import ( - "bytes" - "context" - "encoding/json" + "encoding" "fmt" - "net/http" - "runtime" - "time" + "strings" "github.com/AlekSi/pointer" "go.uber.org/zap" - - "github.com/FerretDB/FerretDB/internal/util/state" - "github.com/FerretDB/FerretDB/internal/util/version" ) -// request represents telemetry request. -type request struct { - Version string `json:"version"` - Commit string `json:"commit"` - Branch string `json:"branch"` - Dirty bool `json:"dirty"` - Debug bool `json:"debug"` - OS string `json:"os"` - Arch string `json:"arch"` - - UUID string `json:"uuid"` - Uptime time.Duration `json:"uptime"` -} - -// response represents telemetry response. -type response struct { - LatestVersion string `json:"latest_version"` -} - -// Reporter sends telemetry reports if telemetry is enabled. -type Reporter struct { - *NewReporterOpts - c *http.Client -} - -// NewReporterOpts represents reporter options. -type NewReporterOpts struct { - URL string - P *state.Provider - L *zap.Logger - UndecidedDelay time.Duration - ReportInterval time.Duration - ReportTimeout time.Duration -} - -// NewReporter creates a new reporter. -func NewReporter(opts *NewReporterOpts) (*Reporter, error) { - return &Reporter{ - NewReporterOpts: opts, - c: http.DefaultClient, - }, nil -} - -// Run runs reporter until context is canceled. -func (r *Reporter) Run(ctx context.Context) { - r.L.Debug("Reporter started.") - defer r.L.Debug("Reporter stopped.") - - ch := r.P.Subscribe() - - r.firstReportDelay(ctx, ch) - - for ctx.Err() == nil { - r.report(ctx) - - delayCtx, delayCancel := context.WithTimeout(ctx, r.ReportInterval) - <-delayCtx.Done() - delayCancel() - } - - // do one last report before exiting if telemetry is explicitly enabled - if pointer.GetBool(r.P.Get().Telemetry) { - r.report(context.Background()) +// parseValue parses a string value into true, false, or nil. +func parseValue(s string) (*bool, error) { + switch strings.ToLower(s) { + case "1", "t", "true", "y", "yes", "on", "enable", "enabled", "optin", "opt-in", "allow": + return pointer.ToBool(true), nil + case "0", "f", "false", "n", "no", "off", "disable", "disabled", "optout", "opt-out", "disallow", "forbid": + return pointer.ToBool(false), nil + case "", "undecided": + return nil, nil + default: + return nil, fmt.Errorf("failed to parse %s", s) } } -// firstReportDelay waits until telemetry reporting state is decided, -// main context is cancelled, or timeout is reached. -func (r *Reporter) firstReportDelay(ctx context.Context, ch <-chan struct{}) { - if r.P.Get().Telemetry != nil { - return - } - - msg := fmt.Sprintf( - "Telemetry state undecided, waiting %s before the first report. "+ - "Read more about FerretDB telemetry at https://beacon.ferretdb.io", - r.UndecidedDelay, - ) - r.L.Info(msg) - - delayCtx, delayCancel := context.WithTimeout(ctx, r.UndecidedDelay) - defer delayCancel() - - for { - select { - case <-delayCtx.Done(): - return - case <-ch: - if r.P.Get().Telemetry != nil { - return - } - } - } +// Flag represents a Kong flag with three states: true, false, and undecided (nil). +type Flag struct { + v *bool } -// makeRequest creates a new telemetry request. -func makeRequest(s *state.State) *request { - v := version.Get() - - return &request{ - Version: v.Version, - Commit: v.Commit, - Branch: v.Branch, - Dirty: v.Dirty, - Debug: v.Debug, - OS: runtime.GOOS, - Arch: runtime.GOARCH, - - UUID: s.UUID, - Uptime: time.Since(s.Start), +// UnmarshalText is used by Kong to parse a flag value. +func (s *Flag) UnmarshalText(text []byte) error { + v, err := parseValue(string(text)) + if err != nil { + return err } -} -// report sends telemetry report unless telemetry is disabled. -func (r *Reporter) report(ctx context.Context) { - s := r.P.Get() - if s.Telemetry != nil && !*s.Telemetry { - r.L.Debug("Telemetry is disabled, skipping reporting.") - return - } + *s = Flag{v: v} - request := makeRequest(s) - r.L.Info("Reporting telemetry.", zap.Reflect("data", request)) + return nil +} - b, err := json.Marshal(request) +// initialState returns initial telemetry state based on: +// - Kong flag value (including `FERRETDB_TELEMETRY` environment variable); +// - common DO_NOT_TRACK environment variable; +// - executable name; +// - and the previously saved state. +func initialState(f *Flag, dnt string, execName string, prev *bool, l *zap.Logger) (*bool, error) { + var disable bool + + // https://consoledonottrack.com is not entirely clear about accepted values. + // Assume that "1", "t", "true", etc. mean that telemetry should be disabled, + // and other valid values, including "0" and empty string, mean undecided. + v, err := parseValue(dnt) if err != nil { - r.L.Error("Failed to marshal telemetry request.", zap.Error(err)) - return + return nil, err } - reqCtx, reqCancel := context.WithTimeout(ctx, r.ReportTimeout) - defer reqCancel() + if pointer.GetBool(v) { + l.Sugar().Infof("Telemetry is disabled by DO_NOT_TRACK=%s environment variable.", dnt) + disable = true + } - req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, r.URL, bytes.NewReader(b)) - if err != nil { - r.L.Error("Failed to create telemetry request.", zap.Error(err)) - return + if strings.Contains(strings.ToLower(execName), "donottrack") { + l.Sugar().Infof("Telemetry is disabled by %q executable name.", execName) + disable = true } - req.Header.Set("Content-Type", "application/json; charset=utf-8") + if disable { + // check for conflicts + if f.v != nil && *f.v { + return nil, fmt.Errorf("telemetry can't be enabled") + } - res, err := r.c.Do(req) - if err != nil { - r.L.Debug("Failed to send telemetry request.", zap.Error(err)) - return + return pointer.ToBool(false), nil } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - r.L.Debug("Failed to send telemetry request.", zap.Int("status", res.StatusCode)) - return - } + if f.v == nil { + if prev == nil { + // undecided state, reporter would log about it during run + return nil, nil + } - var response response - if err = json.NewDecoder(res.Body).Decode(&response); err != nil { - r.L.Debug("Failed to read telemetry response.", zap.Error(err)) - return - } + if *prev { + l.Info("Telemetry is enabled because it was enabled previously.") + } else { + l.Info("Telemetry is disabled because it was disabled previously.") + } - if response.LatestVersion == "" { - r.L.Debug("No latest version in telemetry response.") - return + return prev, nil } - if response.LatestVersion == s.LatestVersion { - r.L.Debug("Latest version is up to date.") - return + if *f.v { + l.Info("Telemetry enabled.") + } else { + l.Info("Telemetry disabled.") } - r.L.Info( - "New version available.", - zap.String("current_version", request.Version), zap.String("latest_version", response.LatestVersion), - ) - - err = r.P.Update(func(s *state.State) { s.LatestVersion = response.LatestVersion }) - if err != nil { - r.L.Error("Failed to update state with latest version.", zap.Error(err)) - return - } + return f.v, nil } + +// check interfaces +var ( + _ encoding.TextUnmarshaler = (*Flag)(nil) +) diff --git a/internal/util/telemetry/telemetry_test.go b/internal/util/telemetry/telemetry_test.go new file mode 100644 index 000000000000..7ca99d4ef6f1 --- /dev/null +++ b/internal/util/telemetry/telemetry_test.go @@ -0,0 +1,78 @@ +// Copyright 2021 FerretDB Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package telemetry + +import ( + "testing" + + "github.com/AlekSi/pointer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "github.com/FerretDB/FerretDB/internal/util/testutil" +) + +func TestState(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { + flag string + dnt string + execName string + prev *bool + state *bool + err string + }{ + "default": {}, + "prev": { + prev: pointer.ToBool(false), + state: pointer.ToBool(false), + }, + "flag": { + flag: "disable", + prev: pointer.ToBool(true), + state: pointer.ToBool(false), + }, + "dnt": { + dnt: "1", + state: pointer.ToBool(false), + }, + "conflict": { + flag: "enable", + execName: "DoNotTrack", + err: "telemetry can't be enabled", + }, + } { + tc := tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var f Flag + err := f.UnmarshalText([]byte(tc.flag)) + require.NoError(t, err) + + logger := testutil.Logger(t, zap.NewAtomicLevelAt(zap.DebugLevel)) + actualState, actualErr := initialState(&f, tc.dnt, tc.execName, tc.prev, logger) + assert.Equal(t, tc.state, actualState) + if tc.err != "" { + assert.EqualError(t, actualErr, tc.err) + return + } + assert.NoError(t, actualErr) + }) + } +} diff --git a/internal/util/testutil/testutil.go b/internal/util/testutil/testutil.go index a7e5a3ff7bf6..0a5bee944084 100644 --- a/internal/util/testutil/testutil.go +++ b/internal/util/testutil/testutil.go @@ -18,6 +18,9 @@ package testutil import ( "context" "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zaptest" ) // Ctx returns test context. @@ -34,3 +37,13 @@ func Ctx(tb testing.TB) context.Context { return ctx } + +// Logger returns zap test logger with valid configuration. +func Logger(tb testing.TB, level zap.AtomicLevel) *zap.Logger { + opts := []zaptest.LoggerOption{ + zaptest.Level(level), + zaptest.WrapOptions(zap.AddCaller(), zap.Development()), + } + + return zaptest.NewLogger(tb, opts...) +}