Skip to content

Commit 1399707

Browse files
authored
feat: add open telemetry (#13)
* chore: init otel configs * feat: integrate OpenTelemetry configuration and setup * feat: enhance OpenTelemetry setup with new stdouttrace exporter and improved configuration * feat: refactor GetLongURL use case and enhance OpenTelemetry integration * feat: enhance OpenTelemetry integration in URL handling and repository use cases * feat: enhance OpenTelemetry metrics integration for URL shortening functionality * feat: improve OpenTelemetry error handling and configuration in main application
1 parent b68b65e commit 1399707

22 files changed

Lines changed: 508 additions & 101 deletions

File tree

backend/.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ COUNTER_START_VAL=14000000
2323

2424
# Log
2525

26-
LOG_LEVEL=debug
26+
LOG_LEVEL=debug
27+
28+
# Otel
29+
30+
SERVICE_NAME=lnk-backend

backend/docker-compose.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,15 @@ services:
1818
- CASSANDRA_AUTHORIZER=CassandraAuthorizer
1919
- CASSANDRA_USER=cassandra
2020
- CASSANDRA_PASSWORD=cassandra
21+
22+
grafana:
23+
image: grafana/otel-lgtm:latest
24+
container_name: lnk-grafana
25+
ports:
26+
- "8081:3000"
27+
- "4317:4317"
28+
environment:
29+
- GF_SECURITY_ADMIN_PASSWORD=admin
30+
depends_on:
31+
- cassandra
32+
- redis

backend/domain/entities/usecases/create_url.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,35 @@ package usecases
33
import (
44
"context"
55
"fmt"
6+
"sync"
67

78
"lnk/domain/entities"
89
"lnk/domain/entities/helpers"
10+
11+
"go.opentelemetry.io/otel"
12+
"go.opentelemetry.io/otel/attribute"
13+
"go.opentelemetry.io/otel/codes"
14+
"go.opentelemetry.io/otel/metric"
15+
)
16+
17+
var (
18+
urlShortenedCounter metric.Int64Counter
19+
counterOnce sync.Once
920
)
1021

1122
func (uc *UseCase) CreateShortURL(ctx context.Context, longURL string) (string, error) {
23+
tracer := otel.Tracer("usecases.CreateShortURL")
24+
ctx, span := tracer.Start(ctx, "CreateShortURLUsecase")
25+
var err error
26+
27+
defer func() {
28+
if err != nil {
29+
span.RecordError(err)
30+
span.SetStatus(codes.Error, err.Error())
31+
}
32+
}()
33+
defer span.End()
34+
1235
id, err := uc.redis.Incr(ctx, uc.counterKey)
1336
if err != nil {
1437
return "", fmt.Errorf("failed to increment counter: %w", err)
@@ -26,5 +49,32 @@ func (uc *UseCase) CreateShortURL(ctx context.Context, longURL string) (string,
2649
return "", fmt.Errorf("failed to create URL in repository: %w", err)
2750
}
2851

52+
uc.incrementURLShortenedMetric(ctx)
53+
2954
return shortCode, nil
3055
}
56+
57+
func (uc *UseCase) incrementURLShortenedMetric(ctx context.Context) {
58+
counterOnce.Do(func() {
59+
meter := otel.Meter("lnk-backend", metric.WithInstrumentationVersion("1.0.0"))
60+
var err error
61+
urlShortenedCounter, err = meter.Int64Counter(
62+
"urls_shortened_total",
63+
metric.WithDescription("Total number of URLs shortened"),
64+
metric.WithUnit("1"),
65+
)
66+
if err != nil {
67+
return
68+
}
69+
})
70+
71+
if urlShortenedCounter == nil {
72+
return
73+
}
74+
75+
urlShortenedCounter.Add(ctx, 1,
76+
metric.WithAttributes(
77+
attribute.String("service", "lnk-backend"),
78+
),
79+
)
80+
}

backend/domain/entities/usecases/get_long_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import (
44
"context"
55
"testing"
66

7-
"github.com/stretchr/testify/mock"
8-
"github.com/stretchr/testify/require"
9-
"go.uber.org/zap"
107
"lnk/domain/entities/usecases"
118
"lnk/extensions/gocqltesting"
129
"lnk/extensions/redis/mocks"
1310
"lnk/gateways/gocql/repositories"
11+
12+
"github.com/stretchr/testify/mock"
13+
"github.com/stretchr/testify/require"
14+
"go.uber.org/zap"
1415
)
1516

1617
func Test_UseCase_GetLongURL(t *testing.T) {
@@ -43,7 +44,7 @@ func Test_UseCase_GetLongURL(t *testing.T) {
4344
require.NoError(t, err)
4445
require.NotEmpty(t, shortCode)
4546

46-
longURL, err := useCase.GetLongURL(shortCode)
47+
longURL, err := useCase.GetLongURL(ctx, shortCode)
4748
require.NoError(t, err)
4849
require.NotEmpty(t, longURL)
4950
require.Equal(t, url, longURL)
@@ -52,6 +53,7 @@ func Test_UseCase_GetLongURL(t *testing.T) {
5253
func Test_UseCase_GetLongURL_NotFound(t *testing.T) {
5354
t.Parallel()
5455

56+
ctx := context.Background()
5557
session, err := gocqltesting.NewDB(t, t.Name())
5658
require.NoError(t, err)
5759

@@ -67,7 +69,7 @@ func Test_UseCase_GetLongURL_NotFound(t *testing.T) {
6769
useCase := usecases.NewUseCase(params)
6870

6971
shortCode := "1234567890"
70-
longURL, err := useCase.GetLongURL(shortCode)
72+
longURL, err := useCase.GetLongURL(ctx, shortCode)
7173
require.Error(t, err)
7274
require.ErrorIs(t, err, usecases.ErrURLNotFound)
7375
require.Empty(t, longURL)

backend/domain/entities/usecases/get_long_url.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
package usecases
22

33
import (
4+
"context"
45
"fmt"
6+
7+
"go.opentelemetry.io/otel"
8+
"go.opentelemetry.io/otel/codes"
59
)
610

7-
func (uc *UseCase) GetLongURL(shortCode string) (string, error) {
8-
url, err := uc.repository.GetURLByShortCode(shortCode)
11+
func (uc *UseCase) GetLongURL(ctx context.Context, shortCode string) (string, error) {
12+
tracer := otel.Tracer("usecases.GetLongURL")
13+
ctx, span := tracer.Start(ctx, "GetLongURLUsecase")
14+
15+
var err error
16+
defer func() {
17+
if err != nil {
18+
span.RecordError(err)
19+
span.SetStatus(codes.Error, err.Error())
20+
}
21+
}()
22+
defer span.End()
923

24+
url, err := uc.repository.GetURLByShortCode(ctx, shortCode)
1025
if url == nil {
1126
return "", ErrURLNotFound
1227
}

backend/domain/entities/usecases/usecase.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ package usecases
33
import (
44
"errors"
55

6-
"go.uber.org/zap"
76
"lnk/extensions/redis"
87
"lnk/gateways/gocql/repositories"
8+
9+
"go.uber.org/zap"
910
)
1011

1112
var ErrURLNotFound = errors.New("URL not found")

backend/extensions/config/config.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ package config
33
import (
44
"fmt"
55

6-
"github.com/joho/godotenv"
7-
"github.com/kelseyhightower/envconfig"
86
"lnk/extensions/logger"
7+
"lnk/extensions/opentelemetry"
98
"lnk/extensions/redis"
109
"lnk/gateways/gocql"
10+
11+
"github.com/joho/godotenv"
12+
"github.com/kelseyhightower/envconfig"
1113
)
1214

1315
type Config struct {
1416
App App
17+
OTel opentelemetry.Config
1518
Logger logger.Config
1619
Gocql gocql.Config
1720
Redis redis.Config
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package opentelemetry
2+
3+
type Config struct {
4+
ServiceName string `envconfig:"SERVICE_NAME" default:"lnk-backend"`
5+
Endpoint string `envconfig:"OTEL_EXPORTER_OTLP_ENDPOINT" default:"localhost:4317"`
6+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package opentelemetry
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"time"
8+
9+
"go.opentelemetry.io/otel"
10+
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
11+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
12+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
13+
"go.opentelemetry.io/otel/metric"
14+
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
15+
"go.opentelemetry.io/otel/sdk/resource"
16+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
17+
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
18+
"go.opentelemetry.io/otel/trace"
19+
)
20+
21+
var Tracer trace.Tracer
22+
var Meter metric.Meter
23+
24+
func SetupOTelSDK(ctx context.Context, cfg *Config) (func(context.Context) error, error) {
25+
traceExp, err := newTraceExporter(ctx, cfg.Endpoint)
26+
if err != nil {
27+
return nil, fmt.Errorf("failed to initialize trace exporter: %w", err)
28+
}
29+
30+
metricReader, err := newMetricExporter(ctx, cfg.Endpoint)
31+
if err != nil {
32+
return nil, fmt.Errorf("failed to initialize metric exporter: %w", err)
33+
}
34+
35+
tp := newTracerProvider(traceExp, cfg.ServiceName)
36+
mp := newMeterProvider(metricReader, cfg.ServiceName)
37+
38+
otel.SetTracerProvider(tp)
39+
otel.SetMeterProvider(mp)
40+
41+
_ = os.Stdout.Sync()
42+
43+
Tracer = tp.Tracer(cfg.ServiceName)
44+
Meter = mp.Meter(cfg.ServiceName)
45+
46+
return func(ctx context.Context) error {
47+
var errs []error
48+
if err := tp.Shutdown(ctx); err != nil {
49+
errs = append(errs, fmt.Errorf("failed to shutdown tracer provider: %w", err))
50+
}
51+
if err := mp.Shutdown(ctx); err != nil {
52+
errs = append(errs, fmt.Errorf("failed to shutdown meter provider: %w", err))
53+
}
54+
if len(errs) > 0 {
55+
return fmt.Errorf("shutdown errors: %v", errs)
56+
}
57+
return nil
58+
}, nil
59+
}
60+
61+
func newTraceExporter(ctx context.Context, endpoint string) (sdktrace.SpanExporter, error) {
62+
client := otlptracegrpc.NewClient(
63+
otlptracegrpc.WithEndpoint(endpoint),
64+
otlptracegrpc.WithInsecure(),
65+
)
66+
67+
traceExporter, err := otlptrace.New(ctx, client)
68+
if err != nil {
69+
return nil, fmt.Errorf("failed to create trace exporter: %w", err)
70+
}
71+
return traceExporter, nil
72+
}
73+
74+
func newMetricExporter(ctx context.Context, endpoint string) (sdkmetric.Reader, error) {
75+
exporter, err := otlpmetricgrpc.New(
76+
ctx,
77+
otlpmetricgrpc.WithEndpoint(endpoint),
78+
otlpmetricgrpc.WithInsecure(),
79+
)
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to create OTLP metric exporter: %w", err)
82+
}
83+
84+
return sdkmetric.NewPeriodicReader(exporter,
85+
sdkmetric.WithInterval(10*time.Second),
86+
), nil
87+
}
88+
89+
func newMeterProvider(reader sdkmetric.Reader, serviceName string) *sdkmetric.MeterProvider {
90+
r, err := resource.Merge(
91+
resource.Default(),
92+
resource.NewWithAttributes(
93+
semconv.SchemaURL,
94+
semconv.ServiceNameKey.String(serviceName),
95+
),
96+
)
97+
98+
if err != nil {
99+
panic(err)
100+
}
101+
102+
return sdkmetric.NewMeterProvider(
103+
sdkmetric.WithReader(reader),
104+
sdkmetric.WithResource(r),
105+
)
106+
}
107+
108+
func newTracerProvider(exp sdktrace.SpanExporter, serviceName string) *sdktrace.TracerProvider {
109+
r, err := resource.Merge(
110+
resource.Default(),
111+
resource.NewWithAttributes(
112+
semconv.SchemaURL,
113+
semconv.ServiceNameKey.String(serviceName),
114+
),
115+
)
116+
117+
if err != nil {
118+
panic(err)
119+
}
120+
121+
return sdktrace.NewTracerProvider(
122+
sdktrace.WithBatcher(exp,
123+
sdktrace.WithBatchTimeout(1*time.Second),
124+
sdktrace.WithMaxExportBatchSize(512),
125+
),
126+
sdktrace.WithResource(r),
127+
)
128+
}

0 commit comments

Comments
 (0)