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 fef8127..e456c37 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -18,3 +18,15 @@ services: - CASSANDRA_AUTHORIZER=CassandraAuthorizer - CASSANDRA_USER=cassandra - CASSANDRA_PASSWORD=cassandra + + grafana: + image: grafana/otel-lgtm:latest + container_name: lnk-grafana + ports: + - "8081:3000" + - "4317:4317" + 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..88386d3 100644 --- a/backend/domain/entities/usecases/create_url.go +++ b/backend/domain/entities/usecases/create_url.go @@ -3,12 +3,35 @@ package usecases import ( "context" "fmt" + "sync" "lnk/domain/entities" "lnk/domain/entities/helpers" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" +) + +var ( + urlShortenedCounter metric.Int64Counter + counterOnce sync.Once ) func (uc *UseCase) CreateShortURL(ctx context.Context, longURL string) (string, error) { + tracer := otel.Tracer("usecases.CreateShortURL") + ctx, span := tracer.Start(ctx, "CreateShortURLUsecase") + 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) @@ -26,5 +49,32 @@ func (uc *UseCase) CreateShortURL(ctx context.Context, longURL string) (string, return "", fmt.Errorf("failed to create URL in repository: %w", err) } + uc.incrementURLShortenedMetric(ctx) + return shortCode, nil } + +func (uc *UseCase) incrementURLShortenedMetric(ctx context.Context) { + counterOnce.Do(func() { + meter := otel.Meter("lnk-backend", metric.WithInstrumentationVersion("1.0.0")) + var err error + urlShortenedCounter, err = meter.Int64Counter( + "urls_shortened_total", + metric.WithDescription("Total number of URLs shortened"), + metric.WithUnit("1"), + ) + if err != nil { + return + } + }) + + if urlShortenedCounter == nil { + return + } + + urlShortenedCounter.Add(ctx, 1, + metric.WithAttributes( + attribute.String("service", "lnk-backend"), + ), + ) +} 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..c5566ec 100644 --- a/backend/domain/entities/usecases/get_long_url.go +++ b/backend/domain/entities/usecases/get_long_url.go @@ -1,12 +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, "GetLongURLUsecase") + + var err error + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + }() + defer span.End() + url, err := uc.repository.GetURLByShortCode(ctx, shortCode) if url == nil { return "", ErrURLNotFound } 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/config/config.go b/backend/extensions/config/config.go index 5960023..08f5e68 100644 --- a/backend/extensions/config/config.go +++ b/backend/extensions/config/config.go @@ -3,15 +3,18 @@ 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 { App App + OTel opentelemetry.Config Logger logger.Config Gocql gocql.Config Redis redis.Config diff --git a/backend/extensions/opentelemetry/config.go b/backend/extensions/opentelemetry/config.go new file mode 100644 index 0000000..a2742bc --- /dev/null +++ b/backend/extensions/opentelemetry/config.go @@ -0,0 +1,6 @@ +package opentelemetry + +type Config struct { + ServiceName string `envconfig:"SERVICE_NAME" default:"lnk-backend"` + Endpoint string `envconfig:"OTEL_EXPORTER_OTLP_ENDPOINT" default:"localhost:4317"` +} diff --git a/backend/extensions/opentelemetry/setup.go b/backend/extensions/opentelemetry/setup.go new file mode 100644 index 0000000..f820a27 --- /dev/null +++ b/backend/extensions/opentelemetry/setup.go @@ -0,0 +1,128 @@ +package opentelemetry + +import ( + "context" + "fmt" + "os" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/metric" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "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" +) + +var Tracer trace.Tracer +var Meter metric.Meter + +func SetupOTelSDK(ctx context.Context, cfg *Config) (func(context.Context) error, error) { + traceExp, err := newTraceExporter(ctx, cfg.Endpoint) + if err != nil { + return nil, fmt.Errorf("failed to initialize trace exporter: %w", err) + } + + metricReader, err := newMetricExporter(ctx, cfg.Endpoint) + if err != nil { + return nil, fmt.Errorf("failed to initialize metric exporter: %w", err) + } + + tp := newTracerProvider(traceExp, cfg.ServiceName) + mp := newMeterProvider(metricReader, cfg.ServiceName) + + otel.SetTracerProvider(tp) + otel.SetMeterProvider(mp) + + _ = os.Stdout.Sync() + + Tracer = tp.Tracer(cfg.ServiceName) + Meter = mp.Meter(cfg.ServiceName) + + return func(ctx context.Context) error { + var errs []error + if err := tp.Shutdown(ctx); err != nil { + errs = append(errs, fmt.Errorf("failed to shutdown tracer provider: %w", err)) + } + if err := mp.Shutdown(ctx); err != nil { + errs = append(errs, fmt.Errorf("failed to shutdown meter provider: %w", err)) + } + if len(errs) > 0 { + return fmt.Errorf("shutdown errors: %v", errs) + } + return nil + }, nil +} + +func newTraceExporter(ctx context.Context, endpoint string) (sdktrace.SpanExporter, error) { + 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) + } + return traceExporter, nil +} + +func newMetricExporter(ctx context.Context, endpoint string) (sdkmetric.Reader, error) { + exporter, err := otlpmetricgrpc.New( + ctx, + otlpmetricgrpc.WithEndpoint(endpoint), + otlpmetricgrpc.WithInsecure(), + ) + if err != nil { + return nil, fmt.Errorf("failed to create OTLP metric exporter: %w", err) + } + + return sdkmetric.NewPeriodicReader(exporter, + sdkmetric.WithInterval(10*time.Second), + ), nil +} + +func newMeterProvider(reader sdkmetric.Reader, serviceName string) *sdkmetric.MeterProvider { + r, err := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String(serviceName), + ), + ) + + if err != nil { + panic(err) + } + + return sdkmetric.NewMeterProvider( + sdkmetric.WithReader(reader), + sdkmetric.WithResource(r), + ) +} + +func newTracerProvider(exp sdktrace.SpanExporter, serviceName string) *sdktrace.TracerProvider { + r, err := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String(serviceName), + ), + ) + + if err != nil { + panic(err) + } + + return sdktrace.NewTracerProvider( + sdktrace.WithBatcher(exp, + 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..711945c 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, "CreateURLRepository") + + var err error + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + }() + defer span.End() + 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, "GetURLByShortCodeRepository") + + var err error + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + }() + defer span.End() + 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..076c8fa 100644 --- a/backend/gateways/http/handlers/urls_handler.go +++ b/backend/gateways/http/handlers/urls_handler.go @@ -2,11 +2,15 @@ package handlers import ( "errors" + "fmt" "net/http" + "lnk/domain/entities/usecases" + "github.com/gin-gonic/gin" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" "go.uber.org/zap" - "lnk/domain/entities/usecases" ) type CreateURLRequest struct { @@ -52,18 +56,36 @@ func NewURLsHandler(logger *zap.Logger, useCase *usecases.UseCase) *URLsHandler // @Failure 500 {object} ErrorResponse // @Router /shorten [post] func (h *URLsHandler) CreateURL(c *gin.Context) { + ctx := c.Request.Context() + tracer := otel.Tracer("handlers.CreateURL") + ctx, span := tracer.Start(ctx, "CreateURLHandler") + + var err error + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + }() + defer span.End() + var req CreateURLRequest - if err := c.ShouldBindJSON(&req); err != nil { + if bindErr := c.ShouldBindJSON(&req); bindErr != nil { + err = fmt.Errorf("failed to bind JSON: %w", bindErr) + span.SetStatus(codes.Error, err.Error()) c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()}) return } - shortURL, err := h.useCase.CreateShortURL(c.Request.Context(), req.URL) + shortURL, err := h.useCase.CreateShortURL(ctx, req.URL) if err != nil { + err = fmt.Errorf("failed to create short URL: %w", err) + span.SetStatus(codes.Error, err.Error()) c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()}) return } + span.SetStatus(codes.Ok, "Short URL created") c.JSON(http.StatusOK, CreateURLResponse{ ShortURL: shortURL, OriginalURL: req.URL, @@ -84,18 +106,33 @@ func (h *URLsHandler) CreateURL(c *gin.Context) { // @Router /{short_url} [get] func (h *URLsHandler) GetURL(c *gin.Context) { shortCode := c.Param("short_url") + ctx := c.Request.Context() + tracer := otel.Tracer("handlers.GetURL") + ctx, span := tracer.Start(ctx, "GetURLHandler") + + var err error + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + }() + defer span.End() - longURL, err := h.useCase.GetLongURL(shortCode) + longURL, err := h.useCase.GetLongURL(ctx, shortCode) if err != nil { if errors.Is(err, usecases.ErrURLNotFound) { + span.SetStatus(codes.Error, err.Error()) c.JSON(http.StatusNotFound, ErrorResponse{Error: err.Error()}) return } + span.SetStatus(codes.Error, err.Error()) c.JSON(http.StatusInternalServerError, ErrorResponse{Error: err.Error()}) return } + span.SetStatus(codes.Ok, "URL found") c.JSON(http.StatusPermanentRedirect, gin.H{"url": longURL}) } diff --git a/backend/go.mod b/backend/go.mod index dde35b9..a459037 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,14 @@ 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/otlpmetric/otlpmetricgrpc 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/metric v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/sdk/metric v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 go.uber.org/zap v1.27.0 ) @@ -32,6 +39,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 @@ -43,6 +51,8 @@ require ( github.com/docker/go-units v0.5.0 // 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 @@ -55,6 +65,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 @@ -85,6 +97,9 @@ 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/instrumentation/net/http/otelhttp v0.63.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 @@ -95,8 +110,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 abd621c..85f7a81 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= @@ -70,6 +72,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= @@ -104,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= @@ -114,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= @@ -132,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= @@ -226,14 +232,26 @@ 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/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.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -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/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +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.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/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +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/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/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.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= @@ -290,6 +308,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 2e31b83..7f9f2a8 100644 --- a/backend/main.go +++ b/backend/main.go @@ -2,23 +2,26 @@ package main import ( "context" + "fmt" "log" "os" "os/signal" "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 +30,16 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + shutdownOTel, err := setupOTelSDK(ctx, cfg) + if err != nil { + appLogger.Fatal("Failed to setup OpenTelemetry", zap.Error(err)) + } + defer func() { + if err := shutdownOTel(ctx); err != nil { + appLogger.Error("Failed to shutdown OpenTelemetry", zap.Error(err)) + } + }() + session := setupDatabase(cfg, appLogger) defer session.Close() @@ -47,6 +60,14 @@ func main() { shutdownServer(ctx, appLogger, server) } +func setupOTelSDK(ctx context.Context, cfg *config.Config) (func(context.Context) error, error) { + shutdown, err := opentelemetry.SetupOTelSDK(ctx, &cfg.OTel) + if err != nil { + return nil, fmt.Errorf("failed to setup OpenTelemetry SDK: %w", err) + } + return shutdown, nil +} + func setupConfigAndLogger() (*config.Config, *zap.Logger) { cfg, err := config.LoadConfig() if err != nil { 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} +