From 6f12e805412b7799b854b0b72c593b80c0bdde94 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Thu, 12 Sep 2024 11:20:32 +0200 Subject: [PATCH 1/4] Add open telemetry --- backend/.env.development | 2 + backend/api/handler/graphql_handler.go | 30 ++++- backend/cli/ctl/cmd_migrate.go | 2 +- backend/cli/ctl/cmd_server.go | 38 +++++- backend/cli/ctl/cmd_tst.go | 2 +- backend/cli/ctl/open_telemetry.go | 149 +++++++++++++++++++++++ backend/go.mod | 36 +++++- backend/go.sum | 75 ++++++++++-- backend/handler/login_handler.go | 19 +++ backend/handler/telemetry.go | 13 ++ backend/persistence/repository/common.go | 4 +- ci/gitlab/backend.yml | 2 +- devbox.json | 2 +- devbox.lock | 24 ++-- 14 files changed, 355 insertions(+), 43 deletions(-) create mode 100644 backend/.env.development create mode 100644 backend/cli/ctl/open_telemetry.go create mode 100644 backend/handler/telemetry.go diff --git a/backend/.env.development b/backend/.env.development new file mode 100644 index 0000000..4f31bdf --- /dev/null +++ b/backend/.env.development @@ -0,0 +1,2 @@ +OPEN_TELEMETRY_ENABLED=1 + diff --git a/backend/api/handler/graphql_handler.go b/backend/api/handler/graphql_handler.go index dc39d23..d5db8c6 100644 --- a/backend/api/handler/graphql_handler.go +++ b/backend/api/handler/graphql_handler.go @@ -2,6 +2,7 @@ package handler import ( "context" + "fmt" "net/http" "time" @@ -12,6 +13,8 @@ import ( "github.com/99designs/gqlgen/graphql/handler/lru" "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/gorilla/websocket" + "github.com/ravilushqa/otelgqlgen" + "go.opentelemetry.io/otel/attribute" "myvendor.mytld/myproject/backend/api" "myvendor.mytld/myproject/backend/api/graph" @@ -23,12 +26,17 @@ import ( type Config struct { EnableTracing bool EnableLogging bool + EnableOpenTelemetry bool DisableRecover bool WebsocketAllowOrigin string // Constant time duration for sensitive operations (e.g. login / request password reset / perform password reset / registration) SensitiveOperationConstantTime time.Duration } +const ( + requestVariablesPrefix = "gql.request.variables" +) + func NewGraphqlHandler(deps api.ResolverDependencies, handlerConfig Config) http.Handler { config := generated.Config{ Resolvers: &graph.Resolver{ @@ -39,7 +47,7 @@ func NewGraphqlHandler(deps api.ResolverDependencies, handlerConfig Config) http }, Directives: generated.DirectiveRoot{ // No op implementation, will be checked in middleware - BypassAuthentication: func(ctx context.Context, obj any, next graphql.Resolver) (res any, err error) { + BypassAuthentication: func(ctx context.Context, _ any, next graphql.Resolver) (res any, err error) { return next(ctx) }, }, @@ -48,6 +56,26 @@ func NewGraphqlHandler(deps api.ResolverDependencies, handlerConfig Config) http srv := newDefaultServer(exec, handlerConfig) srv.SetErrorPresenter(ErrorPresenter) + if handlerConfig.EnableOpenTelemetry { + srv.Use(otelgqlgen.Middleware( + otelgqlgen.WithRequestVariablesAttributesBuilder( + func(requestVariables map[string]any) []attribute.KeyValue { + variables := make([]attribute.KeyValue, 0, len(requestVariables)) + for k, v := range requestVariables { + switch k { + case "password": + v = "********" + } + variables = append(variables, + attribute.String(fmt.Sprintf("%s.%s", requestVariablesPrefix, k), fmt.Sprintf("%+v", v)), + ) + } + return variables + }, + ), + )) + } + if handlerConfig.EnableLogging { srv.AroundFields(graphql_middleware.LoggerFieldMiddleware) } diff --git a/backend/cli/ctl/cmd_migrate.go b/backend/cli/ctl/cmd_migrate.go index 4724aec..0096ec3 100644 --- a/backend/cli/ctl/cmd_migrate.go +++ b/backend/cli/ctl/cmd_migrate.go @@ -12,7 +12,7 @@ func newMigrateCmd() *cli.Command { return &cli.Command{ Name: "migrate", Usage: "Manage database migrations", - Before: func(c *cli.Context) error { + Before: func(_ *cli.Context) error { goose.SetBaseFS(migrations.FS) return nil }, diff --git a/backend/cli/ctl/cmd_server.go b/backend/cli/ctl/cmd_server.go index d781fd4..3aea54e 100644 --- a/backend/cli/ctl/cmd_server.go +++ b/backend/cli/ctl/cmd_server.go @@ -3,6 +3,7 @@ package main import ( "context" "database/sql" + stderrors "errors" "net/http" "os" "os/signal" @@ -19,8 +20,10 @@ import ( "github.com/networkteam/apexlogutils" "github.com/networkteam/apexlogutils/httplog" apexlogutils_middleware "github.com/networkteam/apexlogutils/middleware" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/robfig/cron" "github.com/urfave/cli/v2" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "myvendor.mytld/myproject/backend/api" api_handler "myvendor.mytld/myproject/backend/api/handler" @@ -80,6 +83,12 @@ func newServerCmd() *cli.Command { EnvVars: []string{"SENTRY_RELEASE"}, }, + &cli.BoolFlag{ + Name: "open-telemetry-enabled", + Usage: "Enable open telemetry", + EnvVars: []string{"OPEN_TELEMETRY_ENABLED"}, + }, + &cli.DurationFlag{ Name: "sensitive-operation-constant-time", Usage: "Constant time duration to wait for sensitive operations (e.g. login / request password reset / perform password reset / registration), to prevent timing attacks", @@ -96,7 +105,7 @@ func newServerCmd() *cli.Command { } } -func serverAction(c *cli.Context) error { +func serverAction(c *cli.Context) (err error) { // This action is where the server is set up and dependencies are wired // -- make sure to keep it clean and with clear intention what is done here @@ -104,7 +113,7 @@ func serverAction(c *cli.Context) error { // Initialize sentry defer sentry.Recover() - err := initializeSentry(c, "backend") + err = initializeSentry(c, "backend") if err != nil { return err } @@ -132,18 +141,27 @@ func serverAction(c *cli.Context) error { // Set up signal handling, should be called before starting background processing setupCancelOnSignal(c) - shutdownCronJobs, err := startCronJobs(c, db) + config, err := getConfig(c) if err != nil { return err } - mux := http.NewServeMux() + // Set up OpenTelemetry + otelShutdown, err := setupOTelSDK(c, config) + if err != nil { + return err + } + defer func() { + err = stderrors.Join(err, otelShutdown(context.Background())) + }() - config, err := getConfig(c) + shutdownCronJobs, err := startCronJobs(c, db) if err != nil { return err } + mux := http.NewServeMux() + deps := api.ResolverDependencies{ DB: db, TimeSource: timeSource, @@ -153,6 +171,7 @@ func serverAction(c *cli.Context) error { graphqlHandler := api_handler.NewGraphqlHandler(deps, api_handler.Config{ EnableTracing: false, EnableLogging: true, + EnableOpenTelemetry: c.Bool("open-telemetry-enabled"), DisableRecover: false, WebsocketAllowOrigin: c.String("websocket-allow-origin"), SensitiveOperationConstantTime: c.Duration("sensitive-operation-constant-time"), @@ -163,9 +182,15 @@ func serverAction(c *cli.Context) error { mux.Handle("/", playground.Handler("GraphQL playground", "/query")) } + if c.Bool("open-telemetry-enabled") { + graphqlHandler = otelhttp.NewHandler(graphqlHandler, "/query") + } + mux.Handle("/query", http_api.MiddlewareStackWithAuth(deps, graphqlHandler)) mux.HandleFunc("/healthz", api_handler.NewHealthzHandler(db)) + mux.Handle("/metrics", promhttp.Handler()) + // FIXME RequestID should be replaced by OpenTelemetry (?) rootHandler := apexlogutils_middleware.RequestID( httplog.New( mux, @@ -180,10 +205,11 @@ func serverAction(c *cli.Context) error { log.Infof("Connects to http://%s/ for GraphQL playground", address) } - return serve(c, rootHandler, func(c *cli.Context) error { + err = serve(c, rootHandler, func(_ *cli.Context) error { shutdownCronJobs() return nil }) + return err } func serve(c *cli.Context, handler http.Handler, onShutdown func(c *cli.Context) error) (err error) { diff --git a/backend/cli/ctl/cmd_tst.go b/backend/cli/ctl/cmd_tst.go index 8cfe73c..9815983 100644 --- a/backend/cli/ctl/cmd_tst.go +++ b/backend/cli/ctl/cmd_tst.go @@ -14,7 +14,7 @@ func newTestCmd() *cli.Command { { Name: "preparedb", Usage: "Prepare test database (e.g. install extensions)", - Action: func(c *cli.Context) error { + Action: func(_ *cli.Context) error { return test_db.PrepareTestDatabase() }, }, diff --git a/backend/cli/ctl/open_telemetry.go b/backend/cli/ctl/open_telemetry.go new file mode 100644 index 0000000..92722ab --- /dev/null +++ b/backend/cli/ctl/open_telemetry.go @@ -0,0 +1,149 @@ +package main + +import ( + "context" + "errors" + + apexlog "github.com/apex/log" + "github.com/urfave/cli/v2" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/prometheus" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/log/global" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + + "myvendor.mytld/myproject/backend/domain" +) + +// setupOTelSDK bootstraps the OpenTelemetry pipeline. +// If it does not return an error, make sure to call shutdown for proper cleanup. +func setupOTelSDK(c *cli.Context, config domain.Config) (shutdown func(context.Context) error, err error) { + ctx := c.Context + var shutdownFuncs []func(context.Context) error + + // shutdown calls cleanup functions registered via shutdownFuncs. + // The errors from the calls are joined. + // Each registered cleanup will be invoked once. + shutdown = func(ctx context.Context) error { + var err error + for _, fn := range shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + shutdownFuncs = nil + return err + } + + if !c.Bool("open-telemetry-enabled") { + apexlog.Debug("OpenTelemetry is disabled") + return shutdown, err + } + + apexlog.Info("Setting up OpenTelemetry") + + // handleErr calls shutdown for cleanup and makes sure that all errors are returned. + handleErr := func(inErr error) { + err = errors.Join(inErr, shutdown(ctx)) + } + + // Set up propagator. + prop := newPropagator() + otel.SetTextMapPropagator(prop) + + res, err := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String(config.AppName), + semconv.ServiceVersionKey.String(c.String("sentry-release")), + ), + ) + if err != nil { + handleErr(err) + return shutdown, err + } + + // Set up trace provider. + tracerProvider, err := newTraceProvider(res) + if err != nil { + handleErr(err) + return shutdown, err + } + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + + // Set up meter provider. + meterProvider, err := newMeterProvider(res) + if err != nil { + handleErr(err) + return shutdown, err + } + shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) + otel.SetMeterProvider(meterProvider) + + // Set up logger provider. + loggerProvider, err := newLoggerProvider(res) + if err != nil { + handleErr(err) + return shutdown, err + } + shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown) + global.SetLoggerProvider(loggerProvider) + + return shutdown, err +} + +func newPropagator() propagation.TextMapPropagator { + return propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) +} + +func newTraceProvider(res *resource.Resource) (*trace.TracerProvider, error) { + traceExporter, err := stdouttrace.New( + stdouttrace.WithPrettyPrint(), + ) + if err != nil { + return nil, err + } + + traceProvider := trace.NewTracerProvider( + trace.WithResource(res), + trace.WithBatcher(traceExporter), + // Default is 5s. Set to 1s for demonstrative purposes. + // trace.WithBatchTimeout(time.Second), + ) + return traceProvider, nil +} + +func newMeterProvider(res *resource.Resource) (*metric.MeterProvider, error) { + metricExporter, err := prometheus.New() + if err != nil { + return nil, err + } + + meterProvider := metric.NewMeterProvider( + metric.WithResource(res), + metric.WithReader(metricExporter), + ) + return meterProvider, nil +} + +func newLoggerProvider(res *resource.Resource) (*log.LoggerProvider, error) { + logExporter, err := stdoutlog.New() + if err != nil { + return nil, err + } + + loggerProvider := log.NewLoggerProvider( + log.WithResource(res), + log.WithProcessor(log.NewBatchProcessor(logExporter)), + ) + return loggerProvider, nil +} diff --git a/backend/go.mod b/backend/go.mod index 495f8a8..515722f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -20,13 +20,23 @@ require ( github.com/networkteam/construct/v2 v2.0.1 github.com/networkteam/qrb v0.8.0 github.com/pressly/goose/v3 v3.21.1 + github.com/prometheus/client_golang v1.20.3 github.com/robfig/cron v1.2.0 github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v2 v2.27.2 github.com/vektah/gqlparser/v2 v2.5.16 github.com/wneessen/go-mail v0.4.2 - golang.org/x/crypto v0.25.0 - golang.org/x/term v0.22.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 + go.opentelemetry.io/otel v1.29.0 + go.opentelemetry.io/otel/exporters/prometheus v0.51.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.5.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.29.0 + go.opentelemetry.io/otel/log v0.5.0 + go.opentelemetry.io/otel/sdk v1.29.0 + go.opentelemetry.io/otel/sdk/log v0.5.0 + go.opentelemetry.io/otel/sdk/metric v1.29.0 + golang.org/x/crypto v0.26.0 + golang.org/x/term v0.23.0 ) // Bundled tool for JUnit xml reports of tests in CI @@ -39,7 +49,9 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bitfield/gotestdox v0.2.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/dave/jennifer v1.7.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -49,6 +61,8 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -59,14 +73,20 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.59.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/r3labs/sse/v2 v2.10.0 // indirect + github.com/ravilushqa/otelgqlgen v0.17.0 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/rs/cors v1.10.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -77,14 +97,18 @@ require ( github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go.opentelemetry.io/contrib v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.19.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.23.0 // indirect golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/backend/go.sum b/backend/go.sum index fc6b0ef..498e561 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -26,10 +26,14 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE= github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY= github.com/boumenot/gocover-cobertura v1.2.0 h1:g+VROIASoEHBrEilIyaCmgo7HGm+AV5yKEPLk0qIY+s= github.com/boumenot/gocover-cobertura v1.2.0/go.mod h1:fz7ly8dslE42VRR5ZWLt2OHGDHjkTiA2oNvKgJEjLT0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -69,6 +73,11 @@ github.com/go-jose/go-jose/v4 v4.0.3/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh1 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -114,6 +123,8 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/korylprince/go-graphql-ws v0.3.6 h1:Z4x6bq60ZEls/FBrjyixqTF/S6kT37JtSTFqygM7edU= github.com/korylprince/go-graphql-ws v0.3.6/go.mod h1:RlQMOnD3KJ8qjdY4v4/qv9/XvTUR0crhSmHDFNvufsc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -124,6 +135,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -146,6 +159,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/networkteam/apexlogutils v0.3.0 h1:ty66E2HuFMTphQXKAUhUn994N2mZ2W55GGtiB/GlJJg= @@ -167,8 +182,18 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pressly/goose/v3 v3.21.1 h1:5SSAKKWej8LVVzNLuT6KIvP1eFDuPvxa+B6H0w78buQ= github.com/pressly/goose/v3 v3.21.1/go.mod h1:sqthmzV8PitchEkjecFJII//l43dLOCzfWh8pHEe+vE= +github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= +github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= +github.com/ravilushqa/otelgqlgen v0.17.0 h1:bLwQfKqtj9P24QpjM2sc1ipBm5Fqv2u7DKN5LIpj3g8= +github.com/ravilushqa/otelgqlgen v0.17.0/go.mod h1:orOIikuYsay1y3CmLgd5gsHcT9EsnXwNKmkAplzzYXQ= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= @@ -227,6 +252,30 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/contrib v1.29.0 h1:fLxD2N918DFRlES8q9iv2yE7iIFlaIMZ7ek0D6qJMqk= +go.opentelemetry.io/contrib v1.29.0/go.mod h1:Tmhw9grdWtmXy6DxZNpIAudzYJqLeEM2P6QTZQSRwU8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/prometheus v0.51.0 h1:G7uexXb/K3T+T9fNLCCKncweEtNEBMTO+46hKX5EdKw= +go.opentelemetry.io/otel/exporters/prometheus v0.51.0/go.mod h1:v0mFe5Kk7woIh938mrZBJBmENYquyA0IICrlYm4Y0t4= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.5.0 h1:ThVXnEsdwNcxdBO+r96ci1xbF+PgNjwlk457VNuJODo= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.5.0/go.mod h1:rHWcSmC4q2h3gje/yOq6sAOaq8+UHxN/Ru3BbmDXOfY= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.29.0 h1:X3ZjNp36/WlkSYx0ul2jw4PtbNEDDeLskw3VPsrpYM0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.29.0/go.mod h1:2uL/xnOXh0CHOBFCWXz5u1A4GXLiW+0IQIzVbeOEQ0U= +go.opentelemetry.io/otel/log v0.5.0 h1:x1Pr6Y3gnXgl1iFBwtGy1W/mnzENoK0w0ZoaeOI3i30= +go.opentelemetry.io/otel/log v0.5.0/go.mod h1:NU/ozXeGuOR5/mjCRXYbTC00NFJ3NYuraV/7O78F0rE= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/log v0.5.0 h1:A+9lSjlZGxkQOr7QSBJcuyyYBw79CufQ69saiJLey7o= +go.opentelemetry.io/otel/sdk/log v0.5.0/go.mod h1:zjxIW7sw1IHolZL2KlSAtrUi8JHttoeiQy43Yl3WuVQ= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -239,8 +288,8 @@ golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIi golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= @@ -266,8 +315,8 @@ golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -275,8 +324,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -298,8 +347,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -311,8 +360,8 @@ golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -323,8 +372,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200526224456-8b020aee10d2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -341,6 +390,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/handler/login_handler.go b/backend/handler/login_handler.go index adbc65d..68c35fb 100644 --- a/backend/handler/login_handler.go +++ b/backend/handler/login_handler.go @@ -6,6 +6,7 @@ import ( logger "github.com/apex/log" fog_errors "github.com/friendsofgo/errors" + "go.opentelemetry.io/otel/metric" "myvendor.mytld/myproject/backend/domain" "myvendor.mytld/myproject/backend/persistence/repository" @@ -14,6 +15,20 @@ import ( var ErrLoginInvalidCredentials = std_errors.New("invalid credentials") +//nolint:gochecknoglobals +var ( + loginSuccessCounter = mustInstrument(meter.Int64Counter( + "login.success.counter", + metric.WithDescription("Number of successful logins."), + metric.WithUnit("{call}"), + )) + loginFailedCounter = mustInstrument(meter.Int64Counter( + "login.failed.counter", + metric.WithDescription("Number of failed logins."), + metric.WithUnit("{call}"), + )) +) + func (h *Handler) Login(ctx context.Context, cmd domain.LoginCmd) (err error) { log := logger. FromContext(ctx). @@ -47,6 +62,8 @@ func (h *Handler) Login(ctx context.Context, cmd domain.LoginCmd) (err error) { Warn("Login failed, invalid password") } + loginFailedCounter.Add(ctx, 1) + return ErrLoginInvalidCredentials } @@ -57,6 +74,8 @@ func (h *Handler) Login(ctx context.Context, cmd domain.LoginCmd) (err error) { return fog_errors.Wrap(err, "updating account last login") } + loginSuccessCounter.Add(ctx, 1) + log. WithField("emailAddress", cmd.EmailAddress). WithField("accountID", account.GetAccountID()). diff --git a/backend/handler/telemetry.go b/backend/handler/telemetry.go new file mode 100644 index 0000000..dd8ef4b --- /dev/null +++ b/backend/handler/telemetry.go @@ -0,0 +1,13 @@ +package handler + +import "go.opentelemetry.io/otel" + +//nolint:gochecknoglobals +var meter = otel.Meter("myvendor.mytld/myproject/backend/handler") + +func mustInstrument[T any](instrument T, err error) T { + if err != nil { + panic(err) + } + return instrument +} diff --git a/backend/persistence/repository/common.go b/backend/persistence/repository/common.go index 9486b35..108e0c6 100644 --- a/backend/persistence/repository/common.go +++ b/backend/persistence/repository/common.go @@ -103,13 +103,13 @@ func WithSort(field, order string) PagingOption { } func WithLimit(limit int) PagingOption { - return func(query builder.SelectBuilder, sortFieldMapping map[string]builder.IdentExp) (builder.SelectBuilder, error) { + return func(query builder.SelectBuilder, _ map[string]builder.IdentExp) (builder.SelectBuilder, error) { return query.Limit(builder.Arg(limit)), nil } } func WithOffset(offset int) PagingOption { - return func(query builder.SelectBuilder, sortFieldMapping map[string]builder.IdentExp) (builder.SelectBuilder, error) { + return func(query builder.SelectBuilder, _ map[string]builder.IdentExp) (builder.SelectBuilder, error) { return query.Offset(builder.Arg(offset)), nil } } diff --git a/ci/gitlab/backend.yml b/ci/gitlab/backend.yml index 59fe950..dd2c52f 100644 --- a/ci/gitlab/backend.yml +++ b/ci/gitlab/backend.yml @@ -57,7 +57,7 @@ backend:lint: stage: test needs: [] dependencies: [] - image: registry.networkteam.com/networkteam/docker/golangci-lint:1.55.2 + image: registry.networkteam.com/networkteam/docker/golangci-lint:1.56.1 script: # Write the code coverage report to gl-code-quality-report.json # and print linting issues to stdout in the format: path/to/file:line description. diff --git a/devbox.json b/devbox.json index d135702..4366fa8 100644 --- a/devbox.json +++ b/devbox.json @@ -5,7 +5,7 @@ "python@3.10", "python310Packages.pip", "mailhog@latest", - "golangci-lint@1.55.2", + "golangci-lint@1.56.1", "postgresql_16@latest" ], "include": ["github:networkteam/devbox-plugins?dir=postgresql"], diff --git a/devbox.lock b/devbox.lock index 2d71013..52d03fb 100644 --- a/devbox.lock +++ b/devbox.lock @@ -49,51 +49,51 @@ } } }, - "golangci-lint@1.55.2": { - "last_modified": "2024-02-10T18:15:24Z", - "resolved": "github:NixOS/nixpkgs/10b813040df67c4039086db0f6eaf65c536886c6#golangci-lint", + "golangci-lint@1.56.1": { + "last_modified": "2024-02-12T13:06:46Z", + "resolved": "github:NixOS/nixpkgs/2d627a2a704708673e56346fcb13d25344b8eaf3#golangci-lint", "source": "devbox-search", - "version": "1.55.2", + "version": "1.56.1", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/2hiyqyisiqrrp4g3xqa2c35w63ylp928-golangci-lint-1.55.2", + "path": "/nix/store/4f7s164lywkq0nzv2r80sj7z0kjvw7vs-golangci-lint-1.56.1", "default": true } ], - "store_path": "/nix/store/2hiyqyisiqrrp4g3xqa2c35w63ylp928-golangci-lint-1.55.2" + "store_path": "/nix/store/4f7s164lywkq0nzv2r80sj7z0kjvw7vs-golangci-lint-1.56.1" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/sxprhbskfahbqrfp14agsr4fj51n0fjd-golangci-lint-1.55.2", + "path": "/nix/store/dyv5cy8inj2f3m1ymh5jqisjrss53y7w-golangci-lint-1.56.1", "default": true } ], - "store_path": "/nix/store/sxprhbskfahbqrfp14agsr4fj51n0fjd-golangci-lint-1.55.2" + "store_path": "/nix/store/dyv5cy8inj2f3m1ymh5jqisjrss53y7w-golangci-lint-1.56.1" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/41bra9w6hn5vc8j28rfz1djwab637g5s-golangci-lint-1.55.2", + "path": "/nix/store/v57g47gzxgbwj6193n5zrhzrdrp53g1s-golangci-lint-1.56.1", "default": true } ], - "store_path": "/nix/store/41bra9w6hn5vc8j28rfz1djwab637g5s-golangci-lint-1.55.2" + "store_path": "/nix/store/v57g47gzxgbwj6193n5zrhzrdrp53g1s-golangci-lint-1.56.1" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/6gz1dqhjq45bz1icl8gxry7j0hrib99w-golangci-lint-1.55.2", + "path": "/nix/store/5w4vqr92y2rs826z10lcnkhlv61bcxny-golangci-lint-1.56.1", "default": true } ], - "store_path": "/nix/store/6gz1dqhjq45bz1icl8gxry7j0hrib99w-golangci-lint-1.55.2" + "store_path": "/nix/store/5w4vqr92y2rs826z10lcnkhlv61bcxny-golangci-lint-1.56.1" } } }, From 43910275e5c56a0baef49618a774574759376f97 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 17 Sep 2024 10:47:26 +0200 Subject: [PATCH 2/4] Implement testable telemetry --- backend/.golangci.yml | 3 +- backend/api/graph/admin.resolvers.go | 42 ++++++------- backend/api/graph/authentication.resolvers.go | 6 +- backend/api/graph/resolver.go | 18 ++++++ .../authentication/mutation_login_test.go | 28 ++++++++- backend/api/handler/graphql_handler.go | 9 +-- backend/api/resolver_dependencies.go | 24 +++---- backend/cli/ctl/cmd_server.go | 12 ++-- backend/go.mod | 6 +- backend/handler/handler.go | 18 ++++-- backend/handler/login_handler.go | 19 +----- backend/handler/telemetry.go | 34 ++++++++-- backend/test/graphql/graphql.go | 5 ++ backend/test/telemetry/assert.go | 63 +++++++++++++++++++ ci/gitlab/backend.yml | 2 +- devbox.json | 2 +- devbox.lock | 24 +++---- 17 files changed, 216 insertions(+), 99 deletions(-) create mode 100644 backend/test/telemetry/assert.go diff --git a/backend/.golangci.yml b/backend/.golangci.yml index 4070a86..e36fdbb 100644 --- a/backend/.golangci.yml +++ b/backend/.golangci.yml @@ -6,7 +6,8 @@ run: tests: true output: - format: line-number + formats: + - format: line-number linters: # Disable all linters, so we enable linters explicitly diff --git a/backend/api/graph/admin.resolvers.go b/backend/api/graph/admin.resolvers.go index 97e8f58..8647a46 100644 --- a/backend/api/graph/admin.resolvers.go +++ b/backend/api/graph/admin.resolvers.go @@ -24,15 +24,15 @@ func (r *mutationResolver) CreateAccount(ctx context.Context, role domain_model. } // Only set OrganisationID if the role fits (work around an issue with selecting an organisation and then changing the role in the admin UI) - if domain_model.Role(role) != domain_model.RoleSystemAdministrator { + if role != domain_model.RoleSystemAdministrator { cmd.OrganisationID = helper.ToNullUUID(organisationID) } - err = r.Handler().AccountCreate(ctx, cmd) + err = r.handler.AccountCreate(ctx, cmd) if err != nil { return nil, err } - record, err := r.Finder().QueryAccount(ctx, domain_model.AccountQuery{ + record, err := r.finder.QueryAccount(ctx, domain_model.AccountQuery{ AccountID: cmd.AccountID, }) if err != nil { @@ -44,7 +44,7 @@ func (r *mutationResolver) CreateAccount(ctx context.Context, role domain_model. // UpdateAccount is the resolver for the updateAccount field. func (r *mutationResolver) UpdateAccount(ctx context.Context, id uuid.UUID, role domain_model.Role, emailAddress string, password *string, organisationID *uuid.UUID) (*model.Account, error) { // Fetch previous record to get organisation id - prevRecord, err := r.Finder().QueryAccount(ctx, domain_model.AccountQuery{ + prevRecord, err := r.finder.QueryAccount(ctx, domain_model.AccountQuery{ AccountID: id, }) if err != nil { @@ -56,15 +56,15 @@ func (r *mutationResolver) UpdateAccount(ctx context.Context, id uuid.UUID, role return nil, err } // Only set NewOrganisationID if the role fits (work around an issue with selecting an organisation and then changing the role in the admin UI) - if domain_model.Role(role) != domain_model.RoleSystemAdministrator { + if role != domain_model.RoleSystemAdministrator { cmd.NewOrganisationID = helper.ToNullUUID(organisationID) } - err = r.Handler().AccountUpdate(ctx, cmd) + err = r.handler.AccountUpdate(ctx, cmd) if err != nil { return nil, err } - record, err := r.Finder().QueryAccount(ctx, domain_model.AccountQuery{ + record, err := r.finder.QueryAccount(ctx, domain_model.AccountQuery{ AccountID: id, }) if err != nil { @@ -75,7 +75,7 @@ func (r *mutationResolver) UpdateAccount(ctx context.Context, id uuid.UUID, role // DeleteAccount is the resolver for the deleteAccount field. func (r *mutationResolver) DeleteAccount(ctx context.Context, id uuid.UUID) (*model.Account, error) { - record, err := r.Finder().QueryAccount(ctx, domain_model.AccountQuery{ + record, err := r.finder.QueryAccount(ctx, domain_model.AccountQuery{ AccountID: id, }) if err != nil { @@ -83,7 +83,7 @@ func (r *mutationResolver) DeleteAccount(ctx context.Context, id uuid.UUID) (*mo } cmd := domain_model.NewAccountDeleteCmd(id, record.OrganisationID) - err = r.Handler().AccountDelete(ctx, cmd) + err = r.handler.AccountDelete(ctx, cmd) if err != nil { return nil, err } @@ -97,11 +97,11 @@ func (r *mutationResolver) CreateOrganisation(ctx context.Context, name string) return nil, err } cmd.Name = name - err = r.Handler().OrganisationCreate(ctx, cmd) + err = r.handler.OrganisationCreate(ctx, cmd) if err != nil { return nil, err } - record, err := r.Finder().QueryOrganisation(ctx, domain_model.OrganisationQuery{ + record, err := r.finder.QueryOrganisation(ctx, domain_model.OrganisationQuery{ OrganisationID: cmd.OrganisationID, }) if err != nil { @@ -116,11 +116,11 @@ func (r *mutationResolver) UpdateOrganisation(ctx context.Context, id uuid.UUID, OrganisationID: id, Name: name, } - err := r.Handler().OrganisationUpdate(ctx, cmd) + err := r.handler.OrganisationUpdate(ctx, cmd) if err != nil { return nil, err } - record, err := r.Finder().QueryOrganisation(ctx, domain_model.OrganisationQuery{ + record, err := r.finder.QueryOrganisation(ctx, domain_model.OrganisationQuery{ OrganisationID: id, }) if err != nil { @@ -131,7 +131,7 @@ func (r *mutationResolver) UpdateOrganisation(ctx context.Context, id uuid.UUID, // DeleteOrganisation is the resolver for the deleteOrganisation field. func (r *mutationResolver) DeleteOrganisation(ctx context.Context, id uuid.UUID) (*model.Organisation, error) { - record, err := r.Finder().QueryOrganisation(ctx, domain_model.OrganisationQuery{ + record, err := r.finder.QueryOrganisation(ctx, domain_model.OrganisationQuery{ OrganisationID: id, }) if err != nil { @@ -139,7 +139,7 @@ func (r *mutationResolver) DeleteOrganisation(ctx context.Context, id uuid.UUID) } cmd := domain_model.NewOrganisationDeleteCmd(id) - err = r.Handler().OrganisationDelete(ctx, cmd) + err = r.handler.OrganisationDelete(ctx, cmd) if err != nil { return nil, err } @@ -148,7 +148,7 @@ func (r *mutationResolver) DeleteOrganisation(ctx context.Context, id uuid.UUID) // Account is the resolver for the Account field. func (r *queryResolver) Account(ctx context.Context, id uuid.UUID) (*model.Account, error) { - record, err := r.Finder().QueryAccount(ctx, domain_model.AccountQuery{ + record, err := r.finder.QueryAccount(ctx, domain_model.AccountQuery{ AccountID: id, }) if err == repository.ErrNotFound { @@ -167,7 +167,7 @@ func (r *queryResolver) AllAccounts(ctx context.Context, page *int, perPage *int if err != nil { return nil, err } - records, err := r.Finder().QueryAccounts(ctx, query, paging) + records, err := r.finder.QueryAccounts(ctx, query, paging) if err != nil { return nil, err } @@ -177,7 +177,7 @@ func (r *queryResolver) AllAccounts(ctx context.Context, page *int, perPage *int // AllAccountsMeta is the resolver for the _allAccountsMeta field. func (r *queryResolver) AllAccountsMeta(ctx context.Context, page *int, perPage *int, sortField *string, sortOrder *string, filter *model.AccountFilter) (*model.ListMetadata, error) { query := helper.MapFromAccountFilter(filter) - count, err := r.Finder().CountAccounts(ctx, query) + count, err := r.finder.CountAccounts(ctx, query) if err != nil { return nil, err } @@ -188,7 +188,7 @@ func (r *queryResolver) AllAccountsMeta(ctx context.Context, page *int, perPage // Organisation is the resolver for the Organisation field. func (r *queryResolver) Organisation(ctx context.Context, id uuid.UUID) (*model.Organisation, error) { - record, err := r.Finder().QueryOrganisation(ctx, domain_model.OrganisationQuery{ + record, err := r.finder.QueryOrganisation(ctx, domain_model.OrganisationQuery{ OrganisationID: id, }) if err == repository.ErrNotFound { @@ -207,7 +207,7 @@ func (r *queryResolver) AllOrganisations(ctx context.Context, page *int, perPage if err != nil { return nil, err } - records, err := r.Finder().QueryOrganisations(ctx, query, paging) + records, err := r.finder.QueryOrganisations(ctx, query, paging) if err != nil { return nil, err } @@ -218,7 +218,7 @@ func (r *queryResolver) AllOrganisations(ctx context.Context, page *int, perPage func (r *queryResolver) AllOrganisationsMeta(ctx context.Context, page *int, perPage *int, sortField *string, sortOrder *string, filter *model.OrganisationFilter) (*model.ListMetadata, error) { query := helper.MapToOrganisationsQuery(filter) - count, err := r.Finder().CountOrganisations(ctx, query) + count, err := r.finder.CountOrganisations(ctx, query) if err != nil { return nil, err } diff --git a/backend/api/graph/authentication.resolvers.go b/backend/api/graph/authentication.resolvers.go index 2ef5352..9a4678b 100644 --- a/backend/api/graph/authentication.resolvers.go +++ b/backend/api/graph/authentication.resolvers.go @@ -28,7 +28,7 @@ func (r *mutationResolver) Login(ctx context.Context, credentials model.LoginCre cmd.ExtendedExpiry = true } - account, err := r.Finder().QueryAccountNotAuthorized(ctx, domain.AccountQueryNotAuthorized{ + account, err := r.finder.QueryAccountNotAuthorized(ctx, domain.AccountQueryNotAuthorized{ Opts: helper.AccountQueryOptsFromSelection(ctx, "account"), EmailAddress: &cmd.EmailAddress, }) @@ -41,7 +41,7 @@ func (r *mutationResolver) Login(ctx context.Context, credentials model.LoginCre cmd.Account = account } - err = r.Handler().Login(ctx, cmd) + err = r.handler.Login(ctx, cmd) if err != nil { if fog_errors.Is(err, handler.ErrLoginInvalidCredentials) { return &model.LoginResult{ @@ -91,7 +91,7 @@ func (r *queryResolver) LoginStatus(ctx context.Context) (bool, error) { // CurrentAccount is the resolver for the currentAccount field. func (r *queryResolver) CurrentAccount(ctx context.Context) (*model.Account, error) { authCtx := authentication.GetAuthContext(ctx) - account, err := r.Finder().QueryAccount(ctx, domain.AccountQuery{ + account, err := r.finder.QueryAccount(ctx, domain.AccountQuery{ AccountID: authCtx.AccountID, Opts: helper.AccountQueryOptsFromSelection(ctx), }) diff --git a/backend/api/graph/resolver.go b/backend/api/graph/resolver.go index 66c1602..05cd2e0 100644 --- a/backend/api/graph/resolver.go +++ b/backend/api/graph/resolver.go @@ -2,9 +2,27 @@ package graph import ( "myvendor.mytld/myproject/backend/api" + "myvendor.mytld/myproject/backend/finder" + "myvendor.mytld/myproject/backend/handler" ) type Resolver struct { api.ResolverDependencies api.ResolverConfig + + handler *handler.Handler + finder *finder.Finder +} + +func NewResolver(deps api.ResolverDependencies, config api.ResolverConfig) *Resolver { + return &Resolver{ + ResolverDependencies: deps, + ResolverConfig: config, + handler: handler.NewHandler(deps.DB, deps.Config, handler.Deps{ + TimeSource: deps.TimeSource, + Mailer: deps.Mailer, + MeterProvider: deps.MeterProvider, + }), + finder: finder.NewFinder(deps.DB, deps.TimeSource), + } } diff --git a/backend/api/graph/tests/authentication/mutation_login_test.go b/backend/api/graph/tests/authentication/mutation_login_test.go index 391dee0..4fb1d9c 100644 --- a/backend/api/graph/tests/authentication/mutation_login_test.go +++ b/backend/api/graph/tests/authentication/mutation_login_test.go @@ -1,16 +1,20 @@ package authentication_test import ( + "context" "testing" + "github.com/davecgh/go-spew/spew" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/sdk/metric/metricdata" "myvendor.mytld/myproject/backend/api" "myvendor.mytld/myproject/backend/test" test_db "myvendor.mytld/myproject/backend/test/db" test_graphql "myvendor.mytld/myproject/backend/test/graphql" + test_telemetry "myvendor.mytld/myproject/backend/test/telemetry" ) const loginGQL = ` @@ -71,8 +75,10 @@ func TestMutationResolver_Login_WithSystemAdministrator_Valid(t *testing.T) { var result loginResult + metricsReader, meterProvider := test_telemetry.SetupTestMeter(t) + req := test_graphql.NewRequest(t, query) - resp := test_graphql.Handle(t, api.ResolverDependencies{DB: db, TimeSource: timeSource}, req, &result) + resp := test_graphql.Handle(t, api.ResolverDependencies{DB: db, TimeSource: timeSource, MeterProvider: meterProvider}, req, &result) test_graphql.RequireNoErrors(t, result.GraphqlErrors) require.Nil(t, result.Data.Result.Error) @@ -85,6 +91,13 @@ func TestMutationResolver_Login_WithSystemAdministrator_Valid(t *testing.T) { setCookieHeader := resp.Header().Get("Set-Cookie") assert.NotEmpty(t, setCookieHeader, "Set-Cookie header is set") + { + metricsData := metricdata.ResourceMetrics{} + err := metricsReader.Collect(context.Background(), &metricsData) + require.NoError(t, err) + spew.Dump(metricsData) + } + // Test we can use a restricted field after authentication var loginStatusResult struct { @@ -123,12 +136,23 @@ func TestMutationResolver_Login_WithSystemAdministrator_InvalidPassword(t *testi var result loginResult + metricsReader, meterProvider := test_telemetry.SetupTestMeter(t) + req := test_graphql.NewRequest(t, query) - test_graphql.Handle(t, api.ResolverDependencies{DB: db, TimeSource: timeSource}, req, &result) + test_graphql.Handle(t, api.ResolverDependencies{DB: db, TimeSource: timeSource, MeterProvider: meterProvider}, req, &result) test_graphql.RequireNoErrors(t, result.GraphqlErrors) require.NotNil(t, result.Data.Result.Error, "result.error") assert.Equal(t, "invalidCredentials", result.Data.Result.Error.Code, "result.error.code") + + test_telemetry.AssertMeterCounter(t, metricsReader, "authentication", "login.invalid_credentials", 1) + + { + metricsData := metricdata.ResourceMetrics{} + err := metricsReader.Collect(context.Background(), &metricsData) + require.NoError(t, err) + spew.Dump(metricsData) + } } func TestMutationResolver_Login_WithOrganisationAdministrator_Valid(t *testing.T) { diff --git a/backend/api/handler/graphql_handler.go b/backend/api/handler/graphql_handler.go index d5db8c6..cda39ea 100644 --- a/backend/api/handler/graphql_handler.go +++ b/backend/api/handler/graphql_handler.go @@ -39,12 +39,9 @@ const ( func NewGraphqlHandler(deps api.ResolverDependencies, handlerConfig Config) http.Handler { config := generated.Config{ - Resolvers: &graph.Resolver{ - ResolverDependencies: deps, - ResolverConfig: api.ResolverConfig{ - SensitiveOperationConstantTime: handlerConfig.SensitiveOperationConstantTime, - }, - }, + Resolvers: graph.NewResolver(deps, api.ResolverConfig{ + SensitiveOperationConstantTime: handlerConfig.SensitiveOperationConstantTime, + }), Directives: generated.DirectiveRoot{ // No op implementation, will be checked in middleware BypassAuthentication: func(ctx context.Context, _ any, next graphql.Resolver) (res any, err error) { diff --git a/backend/api/resolver_dependencies.go b/backend/api/resolver_dependencies.go index 9fdf6fc..510b161 100644 --- a/backend/api/resolver_dependencies.go +++ b/backend/api/resolver_dependencies.go @@ -3,27 +3,17 @@ package api import ( "database/sql" + "go.opentelemetry.io/otel/metric" + "myvendor.mytld/myproject/backend/domain" - "myvendor.mytld/myproject/backend/finder" - "myvendor.mytld/myproject/backend/handler" "myvendor.mytld/myproject/backend/mail" ) // ResolverDependencies provides common dependencies for api resolvers type ResolverDependencies struct { - DB *sql.DB - TimeSource domain.TimeSource - Mailer *mail.Mailer - Config domain.Config -} - -func (r ResolverDependencies) Handler() *handler.Handler { - return handler.NewHandler(r.DB, r.Config, handler.Deps{ - TimeSource: r.TimeSource, - Mailer: r.Mailer, - }) -} - -func (r ResolverDependencies) Finder() *finder.Finder { - return finder.NewFinder(r.DB, r.TimeSource) + Config domain.Config + DB *sql.DB + TimeSource domain.TimeSource + MeterProvider metric.MeterProvider + Mailer *mail.Mailer } diff --git a/backend/cli/ctl/cmd_server.go b/backend/cli/ctl/cmd_server.go index 3aea54e..4658df1 100644 --- a/backend/cli/ctl/cmd_server.go +++ b/backend/cli/ctl/cmd_server.go @@ -24,6 +24,7 @@ import ( "github.com/robfig/cron" "github.com/urfave/cli/v2" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" "myvendor.mytld/myproject/backend/api" api_handler "myvendor.mytld/myproject/backend/api/handler" @@ -146,7 +147,7 @@ func serverAction(c *cli.Context) (err error) { return err } - // Set up OpenTelemetry + // Set up OpenTelemetry with global providers otelShutdown, err := setupOTelSDK(c, config) if err != nil { return err @@ -163,10 +164,11 @@ func serverAction(c *cli.Context) (err error) { mux := http.NewServeMux() deps := api.ResolverDependencies{ - DB: db, - TimeSource: timeSource, - Mailer: mailer, - Config: config, + DB: db, + TimeSource: timeSource, + Config: config, + Mailer: mailer, + MeterProvider: otel.GetMeterProvider(), } graphqlHandler := api_handler.NewGraphqlHandler(deps, api_handler.Config{ EnableTracing: false, diff --git a/backend/go.mod b/backend/go.mod index 515722f..39c7171 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,6 +5,7 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 github.com/apex/log v1.9.0 github.com/boumenot/gocover-cobertura v1.2.0 + github.com/davecgh/go-spew v1.1.1 github.com/friendsofgo/errors v0.9.2 github.com/getsentry/sentry-go v0.28.1 github.com/go-jose/go-jose/v4 v4.0.3 @@ -21,6 +22,7 @@ require ( github.com/networkteam/qrb v0.8.0 github.com/pressly/goose/v3 v3.21.1 github.com/prometheus/client_golang v1.20.3 + github.com/ravilushqa/otelgqlgen v0.17.0 github.com/robfig/cron v1.2.0 github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v2 v2.27.2 @@ -32,6 +34,7 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.5.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.29.0 go.opentelemetry.io/otel/log v0.5.0 + go.opentelemetry.io/otel/metric v1.29.0 go.opentelemetry.io/otel/sdk v1.29.0 go.opentelemetry.io/otel/sdk/log v0.5.0 go.opentelemetry.io/otel/sdk/metric v1.29.0 @@ -54,7 +57,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/dave/jennifer v1.7.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/dnephin/pflag v1.0.7 // indirect github.com/fatih/color v1.17.0 // indirect github.com/fatih/structtag v1.2.0 // indirect @@ -86,7 +88,6 @@ require ( github.com/prometheus/common v0.59.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/r3labs/sse/v2 v2.10.0 // indirect - github.com/ravilushqa/otelgqlgen v0.17.0 // indirect github.com/rjeczalik/notify v0.9.3 // indirect github.com/rs/cors v1.10.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -98,7 +99,6 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.opentelemetry.io/contrib v1.29.0 // indirect - go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.19.0 // indirect diff --git a/backend/handler/handler.go b/backend/handler/handler.go index 6499c59..0676e7e 100644 --- a/backend/handler/handler.go +++ b/backend/handler/handler.go @@ -3,6 +3,8 @@ package handler import ( "database/sql" + "go.opentelemetry.io/otel/metric" + "myvendor.mytld/myproject/backend/domain" "myvendor.mytld/myproject/backend/mail" ) @@ -12,18 +14,22 @@ type Handler struct { timeSource domain.TimeSource mailer *mail.Mailer config domain.Config + + instrumentation instrumentation } type Deps struct { - TimeSource domain.TimeSource - Mailer *mail.Mailer + TimeSource domain.TimeSource + Mailer *mail.Mailer + MeterProvider metric.MeterProvider } func NewHandler(db *sql.DB, config domain.Config, deps Deps) *Handler { return &Handler{ - db: db, - config: config, - timeSource: deps.TimeSource, - mailer: deps.Mailer, + db: db, + config: config, + timeSource: deps.TimeSource, + mailer: deps.Mailer, + instrumentation: initInstrumentation(deps.MeterProvider), } } diff --git a/backend/handler/login_handler.go b/backend/handler/login_handler.go index 68c35fb..9d5e511 100644 --- a/backend/handler/login_handler.go +++ b/backend/handler/login_handler.go @@ -6,7 +6,6 @@ import ( logger "github.com/apex/log" fog_errors "github.com/friendsofgo/errors" - "go.opentelemetry.io/otel/metric" "myvendor.mytld/myproject/backend/domain" "myvendor.mytld/myproject/backend/persistence/repository" @@ -15,20 +14,6 @@ import ( var ErrLoginInvalidCredentials = std_errors.New("invalid credentials") -//nolint:gochecknoglobals -var ( - loginSuccessCounter = mustInstrument(meter.Int64Counter( - "login.success.counter", - metric.WithDescription("Number of successful logins."), - metric.WithUnit("{call}"), - )) - loginFailedCounter = mustInstrument(meter.Int64Counter( - "login.failed.counter", - metric.WithDescription("Number of failed logins."), - metric.WithUnit("{call}"), - )) -) - func (h *Handler) Login(ctx context.Context, cmd domain.LoginCmd) (err error) { log := logger. FromContext(ctx). @@ -62,7 +47,7 @@ func (h *Handler) Login(ctx context.Context, cmd domain.LoginCmd) (err error) { Warn("Login failed, invalid password") } - loginFailedCounter.Add(ctx, 1) + h.instrumentation.loginFailedCounter.Add(ctx, 1) return ErrLoginInvalidCredentials } @@ -74,7 +59,7 @@ func (h *Handler) Login(ctx context.Context, cmd domain.LoginCmd) (err error) { return fog_errors.Wrap(err, "updating account last login") } - loginSuccessCounter.Add(ctx, 1) + h.instrumentation.loginSuccessCounter.Add(ctx, 1) log. WithField("emailAddress", cmd.EmailAddress). diff --git a/backend/handler/telemetry.go b/backend/handler/telemetry.go index dd8ef4b..748f758 100644 --- a/backend/handler/telemetry.go +++ b/backend/handler/telemetry.go @@ -1,9 +1,9 @@ package handler -import "go.opentelemetry.io/otel" - -//nolint:gochecknoglobals -var meter = otel.Meter("myvendor.mytld/myproject/backend/handler") +import ( + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/noop" +) func mustInstrument[T any](instrument T, err error) T { if err != nil { @@ -11,3 +11,29 @@ func mustInstrument[T any](instrument T, err error) T { } return instrument } + +type instrumentation struct { + loginSuccessCounter metric.Int64Counter + loginFailedCounter metric.Int64Counter +} + +func initInstrumentation(provider metric.MeterProvider) instrumentation { + if provider == nil { + provider = noop.NewMeterProvider() + } + + meter := provider.Meter("myvendor.mytld/myproject/backend/handler") + + return instrumentation{ + loginSuccessCounter: mustInstrument(meter.Int64Counter( + "login.success.counter", + metric.WithDescription("Number of successful logins."), + metric.WithUnit("{call}"), + )), + loginFailedCounter: mustInstrument(meter.Int64Counter( + "login.failed.counter", + metric.WithDescription("Number of failed logins."), + metric.WithUnit("{call}"), + )), + } +} diff --git a/backend/test/graphql/graphql.go b/backend/test/graphql/graphql.go index 5360ddf..a72749a 100644 --- a/backend/test/graphql/graphql.go +++ b/backend/test/graphql/graphql.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + "go.opentelemetry.io/otel/metric/noop" "golang.org/x/crypto/bcrypt" "myvendor.mytld/myproject/backend/api" @@ -58,6 +59,10 @@ func SetTestDependencies(t *testing.T, deps *api.ResolverDependencies) { sender := fixture.NewSender() deps.Mailer = mail.NewMailer(sender, mail.DefaultConfig(domain.DefaultConfig())) } + + if deps.MeterProvider == nil { + deps.MeterProvider = noop.NewMeterProvider() + } } func Handle(t *testing.T, deps api.ResolverDependencies, req *http.Request, dst interface{}) *httptest.ResponseRecorder { diff --git a/backend/test/telemetry/assert.go b/backend/test/telemetry/assert.go new file mode 100644 index 0000000..bea5a65 --- /dev/null +++ b/backend/test/telemetry/assert.go @@ -0,0 +1,63 @@ +package telemetry + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metric2 "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +func SetupTestMeter(t *testing.T) (*metric.ManualReader, metric2.MeterProvider) { + reader := metric.NewManualReader() + provider := metric.NewMeterProvider(metric.WithReader(reader)) + + t.Cleanup(func() { + err := provider.Shutdown(context.Background()) + assert.NoError(t, err) + }) + + return reader, provider +} + +func AssertMeterCounter(t *testing.T, reader metric.Reader, scope, name string, want int64) { + t.Helper() + + metricsData := metricdata.ResourceMetrics{} + err := reader.Collect(context.Background(), &metricsData) + require.NoError(t, err) + + var scopeMetrics *metricdata.ScopeMetrics + for _, scopeMetrics = range metricsData.ScopeMetrics { + if scopeMetrics.Scope.Name == scope { + break + } + } + if scopeMetrics == nil { + t.Fatalf("metrics for scope %q not found", scope) + } + + var metrics *metricdata.Metrics + for _, metrics = range scopeMetrics.Metrics { + if metrics.Name == name { + break + } + } + if metrics == nil { + t.Fatalf("metrics for name %q not found", name) + } + + agg, ok := metrics.Data.(metricdata.Sum[int64]) + if !ok { + t.Fatalf("metrics for name %q is not a counter", name) + } + var sum int64 + for _, dp := range agg.DataPoints { + sum += dp.Value + } + + assert.Equal(t, want, sum, "sum of metric %q in scope %q", name, scope) +} diff --git a/ci/gitlab/backend.yml b/ci/gitlab/backend.yml index dd2c52f..489e78d 100644 --- a/ci/gitlab/backend.yml +++ b/ci/gitlab/backend.yml @@ -57,7 +57,7 @@ backend:lint: stage: test needs: [] dependencies: [] - image: registry.networkteam.com/networkteam/docker/golangci-lint:1.56.1 + image: registry.networkteam.com/networkteam/docker/golangci-lint:1.57.2 script: # Write the code coverage report to gl-code-quality-report.json # and print linting issues to stdout in the format: path/to/file:line description. diff --git a/devbox.json b/devbox.json index 4366fa8..4e25278 100644 --- a/devbox.json +++ b/devbox.json @@ -5,7 +5,7 @@ "python@3.10", "python310Packages.pip", "mailhog@latest", - "golangci-lint@1.56.1", + "golangci-lint@1.57.2", "postgresql_16@latest" ], "include": ["github:networkteam/devbox-plugins?dir=postgresql"], diff --git a/devbox.lock b/devbox.lock index 52d03fb..993b828 100644 --- a/devbox.lock +++ b/devbox.lock @@ -49,51 +49,51 @@ } } }, - "golangci-lint@1.56.1": { - "last_modified": "2024-02-12T13:06:46Z", - "resolved": "github:NixOS/nixpkgs/2d627a2a704708673e56346fcb13d25344b8eaf3#golangci-lint", + "golangci-lint@1.57.2": { + "last_modified": "2024-05-03T15:42:32Z", + "resolved": "github:NixOS/nixpkgs/5fd8536a9a5932d4ae8de52b7dc08d92041237fc#golangci-lint", "source": "devbox-search", - "version": "1.56.1", + "version": "1.57.2", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/4f7s164lywkq0nzv2r80sj7z0kjvw7vs-golangci-lint-1.56.1", + "path": "/nix/store/jxi6c752jsl4k7w3vzb4i55910j4abb4-golangci-lint-1.57.2", "default": true } ], - "store_path": "/nix/store/4f7s164lywkq0nzv2r80sj7z0kjvw7vs-golangci-lint-1.56.1" + "store_path": "/nix/store/jxi6c752jsl4k7w3vzb4i55910j4abb4-golangci-lint-1.57.2" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/dyv5cy8inj2f3m1ymh5jqisjrss53y7w-golangci-lint-1.56.1", + "path": "/nix/store/g8jacqcy8cr2n2iylqfdfmxlarn2jhfx-golangci-lint-1.57.2", "default": true } ], - "store_path": "/nix/store/dyv5cy8inj2f3m1ymh5jqisjrss53y7w-golangci-lint-1.56.1" + "store_path": "/nix/store/g8jacqcy8cr2n2iylqfdfmxlarn2jhfx-golangci-lint-1.57.2" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/v57g47gzxgbwj6193n5zrhzrdrp53g1s-golangci-lint-1.56.1", + "path": "/nix/store/p9qf3jr68wwgx9538n2nnrjfszhjbcgc-golangci-lint-1.57.2", "default": true } ], - "store_path": "/nix/store/v57g47gzxgbwj6193n5zrhzrdrp53g1s-golangci-lint-1.56.1" + "store_path": "/nix/store/p9qf3jr68wwgx9538n2nnrjfszhjbcgc-golangci-lint-1.57.2" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/5w4vqr92y2rs826z10lcnkhlv61bcxny-golangci-lint-1.56.1", + "path": "/nix/store/2aq2bwy8inbwdh0lma0pcrsawmq9f18d-golangci-lint-1.57.2", "default": true } ], - "store_path": "/nix/store/5w4vqr92y2rs826z10lcnkhlv61bcxny-golangci-lint-1.56.1" + "store_path": "/nix/store/2aq2bwy8inbwdh0lma0pcrsawmq9f18d-golangci-lint-1.57.2" } } }, From d802f82f35dca0f6408f5a4bfe676f02a5b843d6 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 17 Sep 2024 12:09:22 +0200 Subject: [PATCH 3/4] Make test code work --- .../authentication/mutation_login_test.go | 19 +-------- backend/test/telemetry/assert.go | 39 +++++++++++-------- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/backend/api/graph/tests/authentication/mutation_login_test.go b/backend/api/graph/tests/authentication/mutation_login_test.go index 4fb1d9c..bc3f85d 100644 --- a/backend/api/graph/tests/authentication/mutation_login_test.go +++ b/backend/api/graph/tests/authentication/mutation_login_test.go @@ -1,14 +1,11 @@ package authentication_test import ( - "context" "testing" - "github.com/davecgh/go-spew/spew" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/sdk/metric/metricdata" "myvendor.mytld/myproject/backend/api" "myvendor.mytld/myproject/backend/test" @@ -91,12 +88,7 @@ func TestMutationResolver_Login_WithSystemAdministrator_Valid(t *testing.T) { setCookieHeader := resp.Header().Get("Set-Cookie") assert.NotEmpty(t, setCookieHeader, "Set-Cookie header is set") - { - metricsData := metricdata.ResourceMetrics{} - err := metricsReader.Collect(context.Background(), &metricsData) - require.NoError(t, err) - spew.Dump(metricsData) - } + test_telemetry.AssertMeterCounter(t, metricsReader, "myvendor.mytld/myproject/backend/handler", "login.success.counter", 1) // Test we can use a restricted field after authentication @@ -145,14 +137,7 @@ func TestMutationResolver_Login_WithSystemAdministrator_InvalidPassword(t *testi require.NotNil(t, result.Data.Result.Error, "result.error") assert.Equal(t, "invalidCredentials", result.Data.Result.Error.Code, "result.error.code") - test_telemetry.AssertMeterCounter(t, metricsReader, "authentication", "login.invalid_credentials", 1) - - { - metricsData := metricdata.ResourceMetrics{} - err := metricsReader.Collect(context.Background(), &metricsData) - require.NoError(t, err) - spew.Dump(metricsData) - } + test_telemetry.AssertMeterCounter(t, metricsReader, "myvendor.mytld/myproject/backend/handler", "login.failed.counter", 1) } func TestMutationResolver_Login_WithOrganisationAdministrator_Valid(t *testing.T) { diff --git a/backend/test/telemetry/assert.go b/backend/test/telemetry/assert.go index bea5a65..636cc46 100644 --- a/backend/test/telemetry/assert.go +++ b/backend/test/telemetry/assert.go @@ -12,6 +12,8 @@ import ( ) func SetupTestMeter(t *testing.T) (*metric.ManualReader, metric2.MeterProvider) { + t.Helper() + reader := metric.NewManualReader() provider := metric.NewMeterProvider(metric.WithReader(reader)) @@ -30,28 +32,21 @@ func AssertMeterCounter(t *testing.T, reader metric.Reader, scope, name string, err := reader.Collect(context.Background(), &metricsData) require.NoError(t, err) - var scopeMetrics *metricdata.ScopeMetrics - for _, scopeMetrics = range metricsData.ScopeMetrics { - if scopeMetrics.Scope.Name == scope { - break - } - } - if scopeMetrics == nil { + scopeMetrics, found := find(metricsData.ScopeMetrics, func(m metricdata.ScopeMetrics) bool { + return m.Scope.Name == scope + }) + if !found { t.Fatalf("metrics for scope %q not found", scope) } - - var metrics *metricdata.Metrics - for _, metrics = range scopeMetrics.Metrics { - if metrics.Name == name { - break - } - } - if metrics == nil { + metrics, found := find(scopeMetrics.Metrics, func(m metricdata.Metrics) bool { + return m.Name == name + }) + if !found { t.Fatalf("metrics for name %q not found", name) } - agg, ok := metrics.Data.(metricdata.Sum[int64]) - if !ok { + agg, found := metrics.Data.(metricdata.Sum[int64]) + if !found { t.Fatalf("metrics for name %q is not a counter", name) } var sum int64 @@ -61,3 +56,13 @@ func AssertMeterCounter(t *testing.T, reader metric.Reader, scope, name string, assert.Equal(t, want, sum, "sum of metric %q in scope %q", name, scope) } + +func find[T any](slice []T, predicate func(T) bool) (T, bool) { + for _, item := range slice { + if predicate(item) { + return item, true + } + } + var zero T + return zero, false +} From 385eaca95652971d11642b46a5013fe46801406a Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 17 Sep 2024 13:36:17 +0200 Subject: [PATCH 4/4] Disable OpenTelemetry by default for dev --- backend/.env.development | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/.env.development b/backend/.env.development index 4f31bdf..bfeb3a7 100644 --- a/backend/.env.development +++ b/backend/.env.development @@ -1,2 +1,2 @@ -OPEN_TELEMETRY_ENABLED=1 - +# Enable OpenTelemetry for development by setting this to 1 +OPEN_TELEMETRY_ENABLED=0