Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ COUNTER_START_VAL=14000000

# Log

LOG_LEVEL=debug
LOG_LEVEL=debug

# Otel

SERVICE_NAME=lnk-backend
12 changes: 12 additions & 0 deletions backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
50 changes: 50 additions & 0 deletions backend/domain/entities/usecases/create_url.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"),
),
)
}
12 changes: 7 additions & 5 deletions backend/domain/entities/usecases/get_long_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand All @@ -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)
Expand Down
19 changes: 17 additions & 2 deletions backend/domain/entities/usecases/get_long_url.go
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion backend/domain/entities/usecases/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
7 changes: 5 additions & 2 deletions backend/extensions/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions backend/extensions/opentelemetry/config.go
Original file line number Diff line number Diff line change
@@ -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"`
}
128 changes: 128 additions & 0 deletions backend/extensions/opentelemetry/setup.go
Original file line number Diff line number Diff line change
@@ -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),
)
}
Loading
Loading