From e84df1db08f982b982f551a7bb70b7b5c05d4952 Mon Sep 17 00:00:00 2001 From: Joakim Olsson Date: Fri, 13 Jun 2025 11:00:52 +0200 Subject: [PATCH] refactor: remove Sentry integration and replace with OpenTelemetry Remove Sentry dependencies and configuration. Introduce monitoring setup for OpenTelemetry. Update logging to include log format options, and replace Sentry error handling middleware with monitoring handlers for GraphQL playground. Adjust environment variable handling to enhance configuration clarity and flexibility. --- cmd/service/service.go | 63 ++++++-------------------- go.mod | 25 ++++++++++- go.sum | 58 +++++++++++++++++++++--- k8s/config-prod.yaml | 7 +++ k8s/config.yaml | 6 +++ k8s/deploy.yaml | 2 + k8s/secrets.yaml | 2 - logging/log.go | 19 ++++++-- monitoring/graphql.go | 41 +++++++++++++++++ monitoring/otel.go | 100 +++++++++++++++++++++++++++++++++++++++++ monitoring/span.go | 46 +++++++++++++++++++ 11 files changed, 306 insertions(+), 63 deletions(-) create mode 100644 k8s/config-prod.yaml create mode 100644 k8s/config.yaml create mode 100644 monitoring/graphql.go create mode 100644 monitoring/otel.go create mode 100644 monitoring/span.go diff --git a/cmd/service/service.go b/cmd/service/service.go index 79aa232..43cee3a 100644 --- a/cmd/service/service.go +++ b/cmd/service/service.go @@ -19,8 +19,6 @@ import ( "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/playground" "github.com/alecthomas/kong" - "github.com/getsentry/sentry-go" - sentryhttp "github.com/getsentry/sentry-go/http" "github.com/rs/cors" "github.com/sparetimecoders/goamqp" "github.com/vektah/gqlparser/v2/ast" @@ -34,6 +32,7 @@ import ( "gitlab.com/unboundsoftware/schemas/graph/generated" "gitlab.com/unboundsoftware/schemas/logging" "gitlab.com/unboundsoftware/schemas/middleware" + "gitlab.com/unboundsoftware/schemas/monitoring" "gitlab.com/unboundsoftware/schemas/store" ) @@ -41,16 +40,12 @@ type CLI struct { AmqpURL string `name:"amqp-url" env:"AMQP_URL" help:"URL to use to connect to RabbitMQ" default:"amqp://user:password@unbound-control-plane.orb.local:5672/"` Port int `name:"port" env:"PORT" help:"Listen-port for GraphQL API" default:"8080"` LogLevel string `name:"log-level" env:"LOG_LEVEL" help:"The level of logging to use (debug, info, warn, error, fatal)" default:"info"` + LogFormat string `name:"log-format" env:"LOG_FORMAT" help:"The format of logs" default:"text" enum:"otel,json,text"` DatabaseURL string `name:"postgres-url" env:"POSTGRES_URL" help:"URL to use to connect to Postgres" default:"postgres://postgres:postgres@unbound-control-plane.orb.local:5432/schemas?sslmode=disable"` DatabaseDriverName string `name:"db-driver" env:"DB_DRIVER" help:"Driver to use to connect to db" default:"postgres"` Issuer string `name:"issuer" env:"ISSUER" help:"The JWT token issuer to use" default:"unbound.eu.auth0.com"` StrictSSL bool `name:"strict-ssl" env:"STRICT_SSL" help:"Should strict SSL handling be enabled" default:"true"` - SentryConfig -} - -type SentryConfig struct { - DSN string `name:"sentry-dsn" env:"SENTRY_DSN" help:"Sentry dsn" default:""` - Environment string `name:"sentry-environment" env:"SENTRY_ENVIRONMENT" help:"Sentry environment" default:"development"` + Environment string `name:"environment" env:"ENVIRONMENT" help:"The environment we are running in" default:"development" enum:"development,staging,production"` } var buildVersion = "none" @@ -60,7 +55,7 @@ const serviceName = "schemas" func main() { var cli CLI _ = kong.Parse(&cli) - logger := logging.SetupLogger(cli.LogLevel, serviceName, buildVersion) + logger := logging.SetupLogger(cli.LogLevel, cli.LogFormat, serviceName, buildVersion) closeEvents := make(chan error) if err := start( @@ -74,14 +69,17 @@ func main() { } func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(url string) (Connection, error), cli CLI) error { - if err := setupSentry(logger, cli.SentryConfig); err != nil { - return err - } - defer sentry.Flush(2 * time.Second) - rootCtx, rootCancel := context.WithCancel(context.Background()) defer rootCancel() + shutdownFn, err := monitoring.SetupOTelSDK(rootCtx, cli.LogFormat == "otel", serviceName, buildVersion, cli.Environment) + if err != nil { + return err + } + defer func() { + _ = errors.Join(shutdownFn(context.Background())) + }() + db, err := store.SetupDB(cli.DatabaseDriverName, cli.DatabaseURL) if err != nil { return fmt.Errorf("failed to setup DB: %v", err) @@ -224,11 +222,10 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u Cache: lru.New[string](100), }) - sentryHandler := sentryhttp.New(sentryhttp.Options{Repanic: true}) - mux.Handle("/", sentryHandler.HandleFunc(playground.Handler("GraphQL playground", "/query"))) + mux.Handle("/", monitoring.Handler(playground.Handler("GraphQL playground", "/query"))) mux.Handle("/health", http.HandlerFunc(healthFunc)) mux.Handle("/query", cors.AllowAll().Handler( - sentryHandler.Handle( + monitoring.Handler( mw.Middleware().CheckJWT( apiKeyMiddleware.Handler( authMiddleware.Handler(srv), @@ -289,38 +286,6 @@ func healthFunc(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("OK")) } -func setupSentry(logger *slog.Logger, args SentryConfig) error { - if args.Environment == "" { - return fmt.Errorf("no Sentry environment supplied, exiting") - } - cfg := sentry.ClientOptions{ - Dsn: args.DSN, - Environment: args.Environment, - Release: fmt.Sprintf("%s-%s", serviceName, buildVersion), - } - switch args.Environment { - case "development": - cfg.Debug = true - cfg.EnableTracing = false - cfg.TracesSampleRate = 0.0 - case "production": - if args.DSN == "" { - return fmt.Errorf("no DSN supplied for non-dev environment, exiting") - } - cfg.Debug = false - cfg.EnableTracing = true - cfg.TracesSampleRate = 0.01 - default: - return fmt.Errorf("illegal environment %s", args.Environment) - } - - if err := sentry.Init(cfg); err != nil { - return fmt.Errorf("sentry setup: %w", err) - } - logger.With("environment", args.Environment).Info("configured Sentry") - return nil -} - func ConnectAMQP(url string) (Connection, error) { return goamqp.NewFromURL(serviceName, url) } diff --git a/go.mod b/go.mod index ec4ff3e..68ae021 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module gitlab.com/unboundsoftware/schemas -go 1.23.8 +go 1.24.4 require ( github.com/99designs/gqlgen v0.17.74 @@ -8,7 +8,6 @@ require ( github.com/alecthomas/kong v1.11.0 github.com/apex/log v1.9.0 github.com/auth0/go-jwt-middleware/v2 v2.3.0 - github.com/getsentry/sentry-go v0.33.0 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/jmoiron/sqlx v1.4.0 github.com/pkg/errors v0.9.1 @@ -21,17 +20,31 @@ require ( gitlab.com/unboundsoftware/eventsourced/amqp v1.8.1 gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2 gitlab.com/unboundsoftware/eventsourced/pg v1.17.0 + go.opentelemetry.io/contrib/bridges/otelslog v0.11.0 + go.opentelemetry.io/otel v1.36.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 + go.opentelemetry.io/otel/log v0.12.2 + go.opentelemetry.io/otel/sdk v1.36.0 + go.opentelemetry.io/otel/sdk/log v0.12.2 + go.opentelemetry.io/otel/sdk/metric v1.36.0 + go.opentelemetry.io/otel/trace v1.36.0 ) require ( github.com/agnivade/levenshtein v1.2.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mfridman/interpolate v0.0.2 // indirect @@ -47,6 +60,10 @@ require ( github.com/urfave/cli/v2 v2.27.6 // indirect github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/proto/otlp v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.40.0 // indirect @@ -54,5 +71,9 @@ require ( golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.33.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 760ae04..5cfd030 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 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.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= @@ -46,11 +48,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/getsentry/sentry-go v0.33.0 h1:YWyDii0KGVov3xOaamOnF0mjOrqSjBqwv48UEzn7QFg= -github.com/getsentry/sentry-go v0.33.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +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/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= @@ -60,6 +63,8 @@ github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeD github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -67,6 +72,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= @@ -83,8 +90,9 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -102,8 +110,6 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -176,6 +182,36 @@ gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2 h1:8sCnThNHEPB3BQom gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2/go.mod h1:KeLn3U67hxbdFLfeXd0c0LI/r1C5rijbWrfNdARWe98= gitlab.com/unboundsoftware/eventsourced/pg v1.17.0 h1:pUJzMpNPX0GVsffRZXlpKR1d7Ws96KTxJwbLFPpASSc= gitlab.com/unboundsoftware/eventsourced/pg v1.17.0/go.mod h1:WgPrZhyCbsZ3TG2tPUbh2MUjOEaANJjsWi/0hlIwRVU= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/bridges/otelslog v0.11.0 h1:EMIiYTms4Z4m3bBuKp1VmMNRLZcl6j4YbvOPL1IhlWo= +go.opentelemetry.io/contrib/bridges/otelslog v0.11.0/go.mod h1:DIEZmUR7tzuOOVUTDKvkGWtYWSHFV18Qg8+GMb8wPJw= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 h1:gAU726w9J8fwr4qRDqu1GYMNNs4gXrU+Pv20/N1UpB4= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 h1:12vMqzLLNZtXuXbJhSENRg+Vvx+ynNilV8twBLBsXMY= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2/go.mod h1:ZccPZoPOoq8x3Trik/fCsba7DEYDUnN6yX79pgp2BUQ= +go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc= +go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0= +go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY= +go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0= +go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= +go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -209,6 +245,14 @@ golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/k8s/config-prod.yaml b/k8s/config-prod.yaml new file mode 100644 index 0000000..174d873 --- /dev/null +++ b/k8s/config-prod.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: schemas +data: + LOG_FORMAT: "otel" + ENVIRONMENT: "production" diff --git a/k8s/config.yaml b/k8s/config.yaml new file mode 100644 index 0000000..e5d6e36 --- /dev/null +++ b/k8s/config.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: schemas +data: + ENVIRONMENT: "development" diff --git a/k8s/deploy.yaml b/k8s/deploy.yaml index 2d185bb..36037f3 100644 --- a/k8s/deploy.yaml +++ b/k8s/deploy.yaml @@ -57,6 +57,8 @@ spec: - name: api containerPort: 8080 envFrom: + - configMapRef: + name: schemas - secretRef: name: schemas restartPolicy: Always diff --git a/k8s/secrets.yaml b/k8s/secrets.yaml index b45d0ae..31bcb74 100644 --- a/k8s/secrets.yaml +++ b/k8s/secrets.yaml @@ -15,8 +15,6 @@ spec: data: POSTGRES_URL: "postgres://{{ .DB_USERNAME }}:{{ .DB_PASSWORD }}@{{ .DB_HOST }}:{{ .DB_PORT }}/schemas?sslmode=disable" API_KEY: "{{ .API_KEY }}" - SENTRY_DSN: "{{ .SENTRY_DSN }}" - SENTRY_ENVIRONMENT: "{{ .SENTRY_ENVIRONMENT }}" dataFrom: - extract: key: services/schemas diff --git a/logging/log.go b/logging/log.go index e102615..47b72e3 100644 --- a/logging/log.go +++ b/logging/log.go @@ -4,6 +4,8 @@ import ( "context" "log/slog" "os" + + "go.opentelemetry.io/contrib/bridges/otelslog" ) type Logger interface { @@ -18,17 +20,28 @@ type contextKey string const loggerKey = contextKey("logger") -func SetupLogger(logLevel, serviceName, buildVersion string) *slog.Logger { +func SetupLogger(logLevel, logFormat, serviceName, buildVersion string) *slog.Logger { var leveler slog.LevelVar err := leveler.UnmarshalText([]byte(logLevel)) - defaultLogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + handlerOpts := &slog.HandlerOptions{ AddSource: false, Level: leveler.Level(), ReplaceAttr: nil, - })).With("service", serviceName).With("version", buildVersion) + } + var handler slog.Handler + switch logFormat { + case "json": + handler = slog.NewJSONHandler(os.Stdout, handlerOpts) + case "text": + handler = slog.NewTextHandler(os.Stdout, handlerOpts) + case "otel": + handler = otelslog.NewHandler(serviceName, + otelslog.WithVersion(buildVersion)) + } + defaultLogger = slog.New(handler).With("service", serviceName).With("version", buildVersion) if err != nil { defaultLogger.With("err", err).Error("Failed to parse log level") os.Exit(1) diff --git a/monitoring/graphql.go b/monitoring/graphql.go new file mode 100644 index 0000000..7419702 --- /dev/null +++ b/monitoring/graphql.go @@ -0,0 +1,41 @@ +package monitoring + +import ( + "context" + "fmt" + + "github.com/99designs/gqlgen/graphql" + "go.opentelemetry.io/otel/trace" +) + +func AroundOperations(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler { + op := graphql.GetOperationContext(ctx) + spanName := fmt.Sprintf("graphql:operation:%s", op.OperationName) + // Span always injected in the http handler above + sp := trace.SpanFromContext(ctx) + if sp != nil { + sp.SetName(spanName) + } + return next(ctx) +} + +func AroundRootFields(ctx context.Context, next graphql.RootResolver) graphql.Marshaler { + oc := graphql.GetRootFieldContext(ctx) + spanCtx, span := StartSpan(ctx, fmt.Sprintf("graphql:rootfield:%s", oc.Field.Name)) + defer span.Finish() + return next(spanCtx) +} + +func AroundFields(ctx context.Context, next graphql.Resolver) (res any, err error) { + oc := graphql.GetFieldContext(ctx) + var span Span + if oc.IsResolver { + ctx, span = StartSpan(ctx, fmt.Sprintf("graphql:field:%s", oc.Field.Name)) + } + defer func() { + if span != nil { + span.Finish() + } + }() + return next(ctx) +} diff --git a/monitoring/otel.go b/monitoring/otel.go new file mode 100644 index 0000000..8229cd2 --- /dev/null +++ b/monitoring/otel.go @@ -0,0 +1,100 @@ +package monitoring + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + "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/trace" +) + +// SetupOTelSDK bootstraps the OpenTelemetry pipeline. +func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion, environment string) (func(context.Context) error, error) { + if os.Getenv("OTEL_RESOURCE_ATTRIBUTES") == "" { + if err := os.Setenv("OTEL_RESOURCE_ATTRIBUTES", fmt.Sprintf("service.name=%s,service.version=%s,service.environment=%s", serviceName, buildVersion, environment)); err != nil { + return func(context.Context) error { + return nil + }, err + } + } + var shutdownFuncs []func(context.Context) error + if !enabled { + return func(context.Context) error { + return nil + }, nil + } + shutdown := func(ctx context.Context) error { + var err error + for _, fn := range shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + shutdownFuncs = nil + return err + } + + // handleErr calls shutdown for cleanup and makes sure that all errors are returned. + handleErr := func(inErr error) (func(context.Context) error, error) { + return nil, errors.Join(inErr, shutdown(ctx)) + } + + // Set up the propagator. + prop := propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) + otel.SetTextMapPropagator(prop) + + traceExporter, err := otlptracehttp.New(ctx) + if err != nil { + return handleErr(err) + } + shutdownFuncs = append(shutdownFuncs, traceExporter.Shutdown) + + tracerProvider := trace.NewTracerProvider( + trace.WithBatcher(traceExporter, + trace.WithBatchTimeout(5*time.Second)), + ) + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + + logExporter, err := stdoutlog.New() + if err != nil { + return handleErr(err) + } + processor := log.NewSimpleProcessor(logExporter) + logProvider := log.NewLoggerProvider(log.WithProcessor(processor)) + + global.SetLoggerProvider(logProvider) + shutdownFuncs = append(shutdownFuncs, logProvider.Shutdown) + + exp, err := otlpmetrichttp.New(ctx) + if err != nil { + return handleErr(err) + } + meterProvider := metric.NewMeterProvider(metric.WithReader(metric.NewPeriodicReader(exp))) + shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) + + otel.SetMeterProvider(meterProvider) + return shutdown, err +} + +func Handler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)) + spanCtx, s := StartSpan(ctx, "http") + defer s.Finish() + + h.ServeHTTP(w, r.WithContext(spanCtx)) + }) +} diff --git a/monitoring/span.go b/monitoring/span.go new file mode 100644 index 0000000..448c700 --- /dev/null +++ b/monitoring/span.go @@ -0,0 +1,46 @@ +package monitoring + +import ( + "context" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +type Span interface { + Context() context.Context + Finish() +} + +type span struct { + otelSpan trace.Span + ctx context.Context +} + +func (s *span) Finish() { + s.otelSpan.End() +} + +func (s *span) Context() context.Context { + return s.ctx +} + +func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, Span) { + ctx, otelSpan := otel.Tracer("").Start(ctx, name, opts...) + + return ctx, &span{ + otelSpan: otelSpan, + ctx: ctx, + } +} + +type TraceHandlerFunc func(ctx context.Context, name string) (context.Context, func()) + +func (t TraceHandlerFunc) Trace(tx context.Context, name string) (context.Context, func()) { + return t(tx, name) +} + +func Trace(ctx context.Context, name string) (context.Context, func()) { + ctx, s := StartSpan(ctx, name) + return ctx, s.Finish +}