From 290dc08ed0640773ad136f6fe8b72c8af580366c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 17 Nov 2025 22:00:48 -0300 Subject: [PATCH 1/7] chore: init otel configs --- backend/docker-compose.yml | 11 ++ .../domain/entities/usecases/create_url.go | 15 +++ backend/domain/entities/usecases/usecase.go | 3 +- backend/extensions/opentelemetry/config.go | 1 + backend/extensions/opentelemetry/setup.go | 120 ++++++++++++++++++ backend/go.mod | 16 +++ backend/go.sum | 25 ++++ backend/main.go | 18 ++- 8 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 backend/extensions/opentelemetry/config.go create mode 100644 backend/extensions/opentelemetry/setup.go diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index fef8127..1ed4583 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -18,3 +18,14 @@ services: - CASSANDRA_AUTHORIZER=CassandraAuthorizer - CASSANDRA_USER=cassandra - CASSANDRA_PASSWORD=cassandra + + grafana: + image: grafana/otel-lgtm:latest + container_name: lnk-grafana + ports: + - "8081:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + depends_on: + - cassandra + - redis diff --git a/backend/domain/entities/usecases/create_url.go b/backend/domain/entities/usecases/create_url.go index 61a3009..ff0033a 100644 --- a/backend/domain/entities/usecases/create_url.go +++ b/backend/domain/entities/usecases/create_url.go @@ -6,9 +6,24 @@ import ( "lnk/domain/entities" "lnk/domain/entities/helpers" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" ) func (uc *UseCase) CreateShortURL(ctx context.Context, longURL string) (string, error) { + tracer := otel.Tracer("usecases.CreateShortURL") + ctx, span := tracer.Start(ctx, "CreateShortURL") + var err error + + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + }() + defer span.End() + id, err := uc.redis.Incr(ctx, uc.counterKey) if err != nil { return "", fmt.Errorf("failed to increment counter: %w", err) diff --git a/backend/domain/entities/usecases/usecase.go b/backend/domain/entities/usecases/usecase.go index 67ee560..f16710c 100644 --- a/backend/domain/entities/usecases/usecase.go +++ b/backend/domain/entities/usecases/usecase.go @@ -3,9 +3,10 @@ package usecases import ( "errors" - "go.uber.org/zap" "lnk/extensions/redis" "lnk/gateways/gocql/repositories" + + "go.uber.org/zap" ) var ErrURLNotFound = errors.New("URL not found") diff --git a/backend/extensions/opentelemetry/config.go b/backend/extensions/opentelemetry/config.go new file mode 100644 index 0000000..5178e60 --- /dev/null +++ b/backend/extensions/opentelemetry/config.go @@ -0,0 +1 @@ +package opentelemetry diff --git a/backend/extensions/opentelemetry/setup.go b/backend/extensions/opentelemetry/setup.go new file mode 100644 index 0000000..d60545b --- /dev/null +++ b/backend/extensions/opentelemetry/setup.go @@ -0,0 +1,120 @@ +package opentelemetry + +import ( + "context" + "errors" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" + "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/trace" +) + +func SetupOTelSDK(ctx context.Context) (func(context.Context) error, error) { + var shutdownFuncs []func(context.Context) error + var err 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 + } + + // 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) + + // Set up trace provider. + tracerProvider, err := newTracerProvider() + if err != nil { + handleErr(err) + return shutdown, err + } + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + + // Set up meter provider. + meterProvider, err := newMeterProvider() + if err != nil { + handleErr(err) + return shutdown, err + } + shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) + otel.SetMeterProvider(meterProvider) + + // Set up logger provider. + loggerProvider, err := newLoggerProvider() + 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 newTracerProvider() (*trace.TracerProvider, error) { + traceExporter, err := stdouttrace.New( + stdouttrace.WithPrettyPrint()) + if err != nil { + return nil, err + } + + tracerProvider := trace.NewTracerProvider( + trace.WithBatcher(traceExporter, + // Default is 5s. Set to 1s for demonstrative purposes. + trace.WithBatchTimeout(time.Second)), + ) + return tracerProvider, nil +} + +func newMeterProvider() (*metric.MeterProvider, error) { + metricExporter, err := stdoutmetric.New() + if err != nil { + return nil, err + } + + meterProvider := metric.NewMeterProvider( + metric.WithReader(metric.NewPeriodicReader(metricExporter, + // Default is 1m. Set to 3s for demonstrative purposes. + metric.WithInterval(3*time.Second))), + ) + return meterProvider, nil +} + +func newLoggerProvider() (*log.LoggerProvider, error) { + logExporter, err := stdoutlog.New() + if err != nil { + return nil, err + } + + loggerProvider := log.NewLoggerProvider( + log.WithProcessor(log.NewBatchProcessor(logExporter)), + ) + return loggerProvider, nil +} diff --git a/backend/go.mod b/backend/go.mod index dde35b9..f46e9f4 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -41,8 +41,11 @@ require ( github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/spec v0.20.4 // indirect @@ -85,6 +88,19 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect + go.opentelemetry.io/otel/log v0.14.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.20.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index abd621c..579325a 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -70,6 +70,7 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -226,14 +227,38 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s= +go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4= 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/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= +go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= +go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 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/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= diff --git a/backend/main.go b/backend/main.go index 2e31b83..5f750f9 100644 --- a/backend/main.go +++ b/backend/main.go @@ -8,17 +8,19 @@ import ( "syscall" "time" - gocql "github.com/apache/cassandra-gocql-driver/v2" - redis "github.com/redis/go-redis/v9" - "go.uber.org/zap" "lnk/domain/entities/usecases" "lnk/extensions/config" "lnk/extensions/logger" + "lnk/extensions/opentelemetry" redisPackage "lnk/extensions/redis" gocqlPackage "lnk/gateways/gocql" "lnk/gateways/gocql/repositories" httpServer "lnk/gateways/http" "lnk/gateways/http/handlers" + + gocql "github.com/apache/cassandra-gocql-driver/v2" + redis "github.com/redis/go-redis/v9" + "go.uber.org/zap" ) func main() { @@ -27,6 +29,12 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + shutdownOTel, err := setupOTelSDK(ctx) + if err != nil { + appLogger.Fatal("Failed to setup OpenTelemetry", zap.Error(err)) + } + defer shutdownOTel(ctx) + session := setupDatabase(cfg, appLogger) defer session.Close() @@ -47,6 +55,10 @@ func main() { shutdownServer(ctx, appLogger, server) } +func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) { + return opentelemetry.SetupOTelSDK(ctx) +} + func setupConfigAndLogger() (*config.Config, *zap.Logger) { cfg, err := config.LoadConfig() if err != nil { From 1a9df8feea019f8a1ad884d238174073fd1c7d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 17 Nov 2025 22:36:02 -0300 Subject: [PATCH 2/7] feat: integrate OpenTelemetry configuration and setup --- backend/.env.example | 6 +- backend/docker-compose.yml | 1 + backend/extensions/config/config.go | 7 +- backend/extensions/opentelemetry/config.go | 5 + backend/extensions/opentelemetry/setup.go | 125 ++++++--------------- backend/go.mod | 28 +++-- backend/go.sum | 35 +++--- backend/main.go | 6 +- 8 files changed, 90 insertions(+), 123 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 58d1ccb..4219f17 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -23,4 +23,8 @@ COUNTER_START_VAL=14000000 # Log -LOG_LEVEL=debug \ No newline at end of file +LOG_LEVEL=debug + +# Otel + +SERVICE_NAME=lnk-backend \ No newline at end of file diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 1ed4583..e456c37 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -24,6 +24,7 @@ services: container_name: lnk-grafana ports: - "8081:3000" + - "4317:4317" environment: - GF_SECURITY_ADMIN_PASSWORD=admin depends_on: diff --git a/backend/extensions/config/config.go b/backend/extensions/config/config.go index 5960023..2a73f5b 100644 --- a/backend/extensions/config/config.go +++ b/backend/extensions/config/config.go @@ -3,11 +3,13 @@ package config import ( "fmt" - "github.com/joho/godotenv" - "github.com/kelseyhightower/envconfig" "lnk/extensions/logger" + "lnk/extensions/opentelemetry" "lnk/extensions/redis" "lnk/gateways/gocql" + + "github.com/joho/godotenv" + "github.com/kelseyhightower/envconfig" ) type Config struct { @@ -15,6 +17,7 @@ type Config struct { Logger logger.Config Gocql gocql.Config Redis redis.Config + OTel opentelemetry.Config } type App struct { diff --git a/backend/extensions/opentelemetry/config.go b/backend/extensions/opentelemetry/config.go index 5178e60..8ff2c04 100644 --- a/backend/extensions/opentelemetry/config.go +++ b/backend/extensions/opentelemetry/config.go @@ -1 +1,6 @@ package opentelemetry + +type Config struct { + ServiceName string `envconfig:"SERVICE_NAME" default:"lnk-backend"` + Endpoint string `envconfig:"OTEL_EXPORTER_OTLP_ENDPOINT" default:"http://localhost:4317"` +} diff --git a/backend/extensions/opentelemetry/setup.go b/backend/extensions/opentelemetry/setup.go index d60545b..c929686 100644 --- a/backend/extensions/opentelemetry/setup.go +++ b/backend/extensions/opentelemetry/setup.go @@ -2,119 +2,60 @@ package opentelemetry import ( "context" - "errors" - "time" + "fmt" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" - "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" - "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/trace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.37.0" + "go.opentelemetry.io/otel/trace" ) -func SetupOTelSDK(ctx context.Context) (func(context.Context) error, error) { - var shutdownFuncs []func(context.Context) error - var err error +var tracer trace.Tracer - // 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 - } - - // 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) - - // Set up trace provider. - tracerProvider, err := newTracerProvider() +func SetupOTelSDK(ctx context.Context, cfg *Config) (func(context.Context) error, error) { + exp, err := newExporter(ctx, cfg.Endpoint) if err != nil { - handleErr(err) - return shutdown, err + return nil, fmt.Errorf("failed to initialize exporter: %w", err) } - shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) - otel.SetTracerProvider(tracerProvider) - // Set up meter provider. - meterProvider, err := newMeterProvider() - if err != nil { - handleErr(err) - return shutdown, err - } - shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown) - otel.SetMeterProvider(meterProvider) - - // Set up logger provider. - loggerProvider, err := newLoggerProvider() - if err != nil { - handleErr(err) - return shutdown, err - } - shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown) - global.SetLoggerProvider(loggerProvider) + tp := newTracerProvider(exp, cfg.ServiceName) - return shutdown, err -} + defer func() { _ = tp.Shutdown(ctx) }() -func newPropagator() propagation.TextMapPropagator { - return propagation.NewCompositeTextMapPropagator( - propagation.TraceContext{}, - propagation.Baggage{}, - ) -} + otel.SetTracerProvider(tp) -func newTracerProvider() (*trace.TracerProvider, error) { - traceExporter, err := stdouttrace.New( - stdouttrace.WithPrettyPrint()) - if err != nil { - return nil, err - } + tracer = tp.Tracer(cfg.ServiceName) - tracerProvider := trace.NewTracerProvider( - trace.WithBatcher(traceExporter, - // Default is 5s. Set to 1s for demonstrative purposes. - trace.WithBatchTimeout(time.Second)), - ) - return tracerProvider, nil + return func(ctx context.Context) error { + return tp.Shutdown(ctx) + }, nil } -func newMeterProvider() (*metric.MeterProvider, error) { - metricExporter, err := stdoutmetric.New() +func newExporter(ctx context.Context, endpoint string) (sdktrace.SpanExporter, error) { + traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(endpoint), otlptracegrpc.WithInsecure()) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to create trace exporter: %w", err) } + return traceExporter, nil +} - meterProvider := metric.NewMeterProvider( - metric.WithReader(metric.NewPeriodicReader(metricExporter, - // Default is 1m. Set to 3s for demonstrative purposes. - metric.WithInterval(3*time.Second))), +func newTracerProvider(exp sdktrace.SpanExporter, serviceName string) *sdktrace.TracerProvider { + r, err := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(serviceName), + ), ) - return meterProvider, nil -} -func newLoggerProvider() (*log.LoggerProvider, error) { - logExporter, err := stdoutlog.New() if err != nil { - return nil, err + panic(err) } - loggerProvider := log.NewLoggerProvider( - log.WithProcessor(log.NewBatchProcessor(logExporter)), + return sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp), + sdktrace.WithResource(r), ) - return loggerProvider, nil } diff --git a/backend/go.mod b/backend/go.mod index f46e9f4..0680142 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -9,7 +9,6 @@ require ( github.com/gin-gonic/gin v1.11.0 github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556 github.com/golang-migrate/migrate/v4 v4.19.0 - github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/ory/dockertest/v3 v3.12.0 @@ -18,6 +17,15 @@ require ( github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/swag v1.16.6 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 + go.opentelemetry.io/otel/log v0.14.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/sdk/log v0.14.0 + go.opentelemetry.io/otel/sdk/metric v1.38.0 go.uber.org/zap v1.27.0 ) @@ -32,6 +40,7 @@ require ( github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/containerd/continuity v0.4.5 // indirect @@ -41,7 +50,6 @@ require ( github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -58,6 +66,8 @@ require ( github.com/goccy/go-yaml v1.18.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -89,18 +99,10 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect - go.opentelemetry.io/otel/log v0.14.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/sdk v1.38.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.20.0 // indirect @@ -111,8 +113,10 @@ require ( golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect golang.org/x/tools v0.37.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.75.0 // indirect google.golang.org/protobuf v1.36.10 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/inf.v0 v0.9.1 // 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 579325a..fc4f600 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -30,6 +30,8 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/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/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= @@ -105,6 +107,8 @@ github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556 h1:N/MD/sr6o61X+iZBAT2 github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -115,6 +119,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -133,7 +139,6 @@ github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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= @@ -227,38 +232,34 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s= -go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4= -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/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8= go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM= go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg= go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM= +go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= 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/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= @@ -315,6 +316,14 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/main.go b/backend/main.go index 5f750f9..02e33e8 100644 --- a/backend/main.go +++ b/backend/main.go @@ -29,7 +29,7 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - shutdownOTel, err := setupOTelSDK(ctx) + shutdownOTel, err := setupOTelSDK(ctx, cfg) if err != nil { appLogger.Fatal("Failed to setup OpenTelemetry", zap.Error(err)) } @@ -55,8 +55,8 @@ func main() { shutdownServer(ctx, appLogger, server) } -func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) { - return opentelemetry.SetupOTelSDK(ctx) +func setupOTelSDK(ctx context.Context, cfg *config.Config) (func(context.Context) error, error) { + return opentelemetry.SetupOTelSDK(ctx, &cfg.OTel) } func setupConfigAndLogger() (*config.Config, *zap.Logger) { From d6c39ef6f896ea7eab1375ed926b5d5b7737343e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Mon, 17 Nov 2025 23:06:19 -0300 Subject: [PATCH 3/7] feat: enhance OpenTelemetry setup with new stdouttrace exporter and improved configuration --- backend/extensions/opentelemetry/config.go | 2 +- backend/extensions/opentelemetry/setup.go | 26 ++++++++++++++++------ backend/go.mod | 1 + 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/backend/extensions/opentelemetry/config.go b/backend/extensions/opentelemetry/config.go index 8ff2c04..a2742bc 100644 --- a/backend/extensions/opentelemetry/config.go +++ b/backend/extensions/opentelemetry/config.go @@ -2,5 +2,5 @@ package opentelemetry type Config struct { ServiceName string `envconfig:"SERVICE_NAME" default:"lnk-backend"` - Endpoint string `envconfig:"OTEL_EXPORTER_OTLP_ENDPOINT" default:"http://localhost:4317"` + Endpoint string `envconfig:"OTEL_EXPORTER_OTLP_ENDPOINT" default:"localhost:4317"` } diff --git a/backend/extensions/opentelemetry/setup.go b/backend/extensions/opentelemetry/setup.go index c929686..38ccb80 100644 --- a/backend/extensions/opentelemetry/setup.go +++ b/backend/extensions/opentelemetry/setup.go @@ -3,8 +3,11 @@ package opentelemetry import ( "context" "fmt" + "os" + "time" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -12,7 +15,7 @@ import ( "go.opentelemetry.io/otel/trace" ) -var tracer trace.Tracer +var Tracer trace.Tracer func SetupOTelSDK(ctx context.Context, cfg *Config) (func(context.Context) error, error) { exp, err := newExporter(ctx, cfg.Endpoint) @@ -22,11 +25,12 @@ func SetupOTelSDK(ctx context.Context, cfg *Config) (func(context.Context) error tp := newTracerProvider(exp, cfg.ServiceName) - defer func() { _ = tp.Shutdown(ctx) }() - otel.SetTracerProvider(tp) + + fmt.Fprintf(os.Stdout, "[DEBUG] OpenTelemetry tracer initialized, exporting to: %s\n", cfg.Endpoint) + os.Stdout.Sync() - tracer = tp.Tracer(cfg.ServiceName) + Tracer = tp.Tracer(cfg.ServiceName) return func(ctx context.Context) error { return tp.Shutdown(ctx) @@ -34,7 +38,12 @@ func SetupOTelSDK(ctx context.Context, cfg *Config) (func(context.Context) error } func newExporter(ctx context.Context, endpoint string) (sdktrace.SpanExporter, error) { - traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(endpoint), otlptracegrpc.WithInsecure()) + client := otlptracegrpc.NewClient( + otlptracegrpc.WithEndpoint(endpoint), + otlptracegrpc.WithInsecure(), + ) + + traceExporter, err := otlptrace.New(ctx, client) if err != nil { return nil, fmt.Errorf("failed to create trace exporter: %w", err) } @@ -46,7 +55,7 @@ func newTracerProvider(exp sdktrace.SpanExporter, serviceName string) *sdktrace. resource.Default(), resource.NewWithAttributes( semconv.SchemaURL, - semconv.ServiceName(serviceName), + semconv.ServiceNameKey.String(serviceName), ), ) @@ -55,7 +64,10 @@ func newTracerProvider(exp sdktrace.SpanExporter, serviceName string) *sdktrace. } return sdktrace.NewTracerProvider( - sdktrace.WithBatcher(exp), + sdktrace.WithBatcher(exp, + sdktrace.WithBatchTimeout(1*time.Second), // Flush batches every 1 second + sdktrace.WithMaxExportBatchSize(512), // Export up to 512 spans per batch + ), sdktrace.WithResource(r), ) } diff --git a/backend/go.mod b/backend/go.mod index 0680142..044690d 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -22,6 +22,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 go.opentelemetry.io/otel/log v0.14.0 go.opentelemetry.io/otel/sdk v1.38.0 go.opentelemetry.io/otel/sdk/log v0.14.0 From 1225ba27c543a08761199adf9b9d4483c593fcd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Alves?= Date: Tue, 18 Nov 2025 22:48:13 -0300 Subject: [PATCH 4/7] feat: refactor GetLongURL use case and enhance OpenTelemetry integration --- .../domain/entities/usecases/get_long_test.go | 12 +-- .../domain/entities/usecases/get_long_url.go | 20 ++++- backend/extensions/opentelemetry/setup.go | 9 +- .../gocql/repositories/url_repository.go | 37 +++++++-- .../gateways/http/handlers/urls_handler.go | 5 +- frontend/src/app/[shortUrl]/route.ts | 2 - frontend/src/app/globals.css | 6 ++ frontend/src/app/layout.tsx | 3 +- frontend/src/components/ui/sonner.tsx | 2 + frontend/src/components/url-dialog.tsx | 31 +++---- frontend/src/components/url-input.tsx | 37 +++++---- frontend/src/components/url-shortener.tsx | 28 +------ .../src/providers/url-shortener-provider.tsx | 83 +++++++++++++++++++ 13 files changed, 189 insertions(+), 86 deletions(-) create mode 100644 frontend/src/providers/url-shortener-provider.tsx diff --git a/backend/domain/entities/usecases/get_long_test.go b/backend/domain/entities/usecases/get_long_test.go index 49a2657..7819e13 100644 --- a/backend/domain/entities/usecases/get_long_test.go +++ b/backend/domain/entities/usecases/get_long_test.go @@ -4,13 +4,14 @@ import ( "context" "testing" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "go.uber.org/zap" "lnk/domain/entities/usecases" "lnk/extensions/gocqltesting" "lnk/extensions/redis/mocks" "lnk/gateways/gocql/repositories" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.uber.org/zap" ) func Test_UseCase_GetLongURL(t *testing.T) { @@ -43,7 +44,7 @@ func Test_UseCase_GetLongURL(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, shortCode) - longURL, err := useCase.GetLongURL(shortCode) + longURL, err := useCase.GetLongURL(ctx, shortCode) require.NoError(t, err) require.NotEmpty(t, longURL) require.Equal(t, url, longURL) @@ -52,6 +53,7 @@ func Test_UseCase_GetLongURL(t *testing.T) { func Test_UseCase_GetLongURL_NotFound(t *testing.T) { t.Parallel() + ctx := context.Background() session, err := gocqltesting.NewDB(t, t.Name()) require.NoError(t, err) @@ -67,7 +69,7 @@ func Test_UseCase_GetLongURL_NotFound(t *testing.T) { useCase := usecases.NewUseCase(params) shortCode := "1234567890" - longURL, err := useCase.GetLongURL(shortCode) + longURL, err := useCase.GetLongURL(ctx, shortCode) require.Error(t, err) require.ErrorIs(t, err, usecases.ErrURLNotFound) require.Empty(t, longURL) diff --git a/backend/domain/entities/usecases/get_long_url.go b/backend/domain/entities/usecases/get_long_url.go index 075bfa7..12e2518 100644 --- a/backend/domain/entities/usecases/get_long_url.go +++ b/backend/domain/entities/usecases/get_long_url.go @@ -1,11 +1,27 @@ package usecases import ( + "context" "fmt" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" ) -func (uc *UseCase) GetLongURL(shortCode string) (string, error) { - url, err := uc.repository.GetURLByShortCode(shortCode) +func (uc *UseCase) GetLongURL(ctx context.Context, shortCode string) (string, error) { + tracer := otel.Tracer("usecases.GetLongURL") + ctx, span := tracer.Start(ctx, "GetLongURL") + defer span.End() + + var err error + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + }() + + url, err := uc.repository.GetURLByShortCode(ctx, shortCode) if url == nil { return "", ErrURLNotFound diff --git a/backend/extensions/opentelemetry/setup.go b/backend/extensions/opentelemetry/setup.go index 38ccb80..b340e6f 100644 --- a/backend/extensions/opentelemetry/setup.go +++ b/backend/extensions/opentelemetry/setup.go @@ -26,8 +26,7 @@ func SetupOTelSDK(ctx context.Context, cfg *Config) (func(context.Context) error tp := newTracerProvider(exp, cfg.ServiceName) otel.SetTracerProvider(tp) - - fmt.Fprintf(os.Stdout, "[DEBUG] OpenTelemetry tracer initialized, exporting to: %s\n", cfg.Endpoint) + os.Stdout.Sync() Tracer = tp.Tracer(cfg.ServiceName) @@ -42,7 +41,7 @@ func newExporter(ctx context.Context, endpoint string) (sdktrace.SpanExporter, e otlptracegrpc.WithEndpoint(endpoint), otlptracegrpc.WithInsecure(), ) - + traceExporter, err := otlptrace.New(ctx, client) if err != nil { return nil, fmt.Errorf("failed to create trace exporter: %w", err) @@ -65,8 +64,8 @@ func newTracerProvider(exp sdktrace.SpanExporter, serviceName string) *sdktrace. return sdktrace.NewTracerProvider( sdktrace.WithBatcher(exp, - sdktrace.WithBatchTimeout(1*time.Second), // Flush batches every 1 second - sdktrace.WithMaxExportBatchSize(512), // Export up to 512 spans per batch + sdktrace.WithBatchTimeout(1*time.Second), + sdktrace.WithMaxExportBatchSize(512), ), sdktrace.WithResource(r), ) diff --git a/backend/gateways/gocql/repositories/url_repository.go b/backend/gateways/gocql/repositories/url_repository.go index 7c19d3f..ccdde54 100644 --- a/backend/gateways/gocql/repositories/url_repository.go +++ b/backend/gateways/gocql/repositories/url_repository.go @@ -6,14 +6,29 @@ import ( "fmt" "time" - "github.com/gocql/gocql" "lnk/domain/entities" + + "github.com/gocql/gocql" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" ) func (r *Repository) CreateURL(ctx context.Context, url *entities.URL) error { + tracer := otel.Tracer("repositories.CreateURL") + ctx, span := tracer.Start(ctx, "CreateURL") + defer span.End() + + var err error + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + }() + url.CreatedAt = time.Now().UTC() - err := r.session.Query( + err = r.session.Query( "INSERT INTO urls (short_code, long_url, created_at) VALUES (?, ?, ?)", url.ShortCode, url.LongURL, url.CreatedAt, ).ExecContext(ctx) @@ -24,13 +39,25 @@ func (r *Repository) CreateURL(ctx context.Context, url *entities.URL) error { return nil } -func (r *Repository) GetURLByShortCode(shortCode string) (*entities.URL, error) { +func (r *Repository) GetURLByShortCode(ctx context.Context, shortCode string) (*entities.URL, error) { + tracer := otel.Tracer("repositories.GetURLByShortCode") + ctx, span := tracer.Start(ctx, "GetURLByShortCode") + defer span.End() + + var err error + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + }() + var url entities.URL - err := r.session.Query( + err = r.session.Query( "SELECT short_code, long_url, created_at FROM urls WHERE short_code = ?", shortCode, - ).Scan(&url.ShortCode, &url.LongURL, &url.CreatedAt) + ).ScanContext(ctx, &url.ShortCode, &url.LongURL, &url.CreatedAt) if err != nil { if errors.Is(err, gocql.ErrNotFound) { return nil, gocql.ErrNotFound diff --git a/backend/gateways/http/handlers/urls_handler.go b/backend/gateways/http/handlers/urls_handler.go index e62fa2e..e3d78c1 100644 --- a/backend/gateways/http/handlers/urls_handler.go +++ b/backend/gateways/http/handlers/urls_handler.go @@ -4,9 +4,10 @@ import ( "errors" "net/http" + "lnk/domain/entities/usecases" + "github.com/gin-gonic/gin" "go.uber.org/zap" - "lnk/domain/entities/usecases" ) type CreateURLRequest struct { @@ -85,7 +86,7 @@ func (h *URLsHandler) CreateURL(c *gin.Context) { func (h *URLsHandler) GetURL(c *gin.Context) { shortCode := c.Param("short_url") - longURL, err := h.useCase.GetLongURL(shortCode) + longURL, err := h.useCase.GetLongURL(c.Request.Context(), shortCode) if err != nil { if errors.Is(err, usecases.ErrURLNotFound) { c.JSON(http.StatusNotFound, ErrorResponse{Error: err.Error()}) diff --git a/frontend/src/app/[shortUrl]/route.ts b/frontend/src/app/[shortUrl]/route.ts index 32c44a6..9714448 100644 --- a/frontend/src/app/[shortUrl]/route.ts +++ b/frontend/src/app/[shortUrl]/route.ts @@ -19,8 +19,6 @@ export async function GET( try { const response = await getShortUrl(shortUrl); - console.log("response", response); - if (response.status === 308) { const location = response.headers.get("Location"); if (location) { diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index ae1e47d..6d42995 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -99,3 +99,9 @@ background-color: #0b101b; } } + +@layer utilities { + [data-sonner-toast][data-type="success"] [data-icon] { + color: #30b6db; + } +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index a13103a..0bd7eb1 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { Toaster } from "@/components/ui/sonner"; +import { UrlShortenerProvider } from "@/providers/url-shortener-provider"; const _geist = Geist({ subsets: ["latin"] }); const _geistMono = Geist_Mono({ subsets: ["latin"] }); @@ -38,7 +39,7 @@ export default function RootLayout({ return ( - {children} + {children} diff --git a/frontend/src/components/ui/sonner.tsx b/frontend/src/components/ui/sonner.tsx index 0921376..e43878e 100644 --- a/frontend/src/components/ui/sonner.tsx +++ b/frontend/src/components/ui/sonner.tsx @@ -17,6 +17,8 @@ const Toaster = ({ ...props }: ToasterProps) => { "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", + success: + "group-[.toaster]:border-l-[4px] group-[.toaster]:border-[#30b6db]", }, }} {...props} diff --git a/frontend/src/components/url-dialog.tsx b/frontend/src/components/url-dialog.tsx index 63fba01..69feeba 100644 --- a/frontend/src/components/url-dialog.tsx +++ b/frontend/src/components/url-dialog.tsx @@ -1,6 +1,7 @@ import { CheckCircle2, Copy } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; +import { useUrlShortener } from "@/providers/url-shortener-provider"; import { Button } from "./ui/button"; import { Dialog, @@ -11,21 +12,9 @@ import { } from "./ui/dialog"; import { Input } from "./ui/input"; -interface UrlDialogProps { - shortUrl: string; - originalUrl: string; - open: boolean; - setUrl: (url: string) => void; - onOpenChange: (open: boolean) => void; -} - -export function UrlDialog({ - shortUrl, - originalUrl, - open, - setUrl, - onOpenChange, -}: UrlDialogProps) { +export function UrlDialog() { + const { shortUrl, originalUrl, dialogOpen, setDialogOpen, clearUrl } = + useUrlShortener(); const [copied, setCopied] = useState(false); const frontEndUrl = process.env.NEXT_PUBLIC_FRONT_URL; const fullUrl = `${frontEndUrl}/${shortUrl}`; @@ -42,8 +31,13 @@ export function UrlDialog({ } }; + const handleNewUrl = () => { + clearUrl(); + setDialogOpen(false); + }; + return ( - + @@ -84,10 +78,7 @@ export function UrlDialog({