From 15b38ad138b9d1eb30cf69aab30c5bd3328fe93f Mon Sep 17 00:00:00 2001 From: KostLinux Date: Sun, 26 Jan 2025 00:02:04 +0200 Subject: [PATCH 1/8] feature: replace util package with random --- pkg/{util/random.go => random/string.go} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename pkg/{util/random.go => random/string.go} (78%) diff --git a/pkg/util/random.go b/pkg/random/string.go similarity index 78% rename from pkg/util/random.go rename to pkg/random/string.go index 9710b84..4f2cd5b 100644 --- a/pkg/util/random.go +++ b/pkg/random/string.go @@ -1,11 +1,11 @@ -package util +package random import ( "crypto/rand" "encoding/hex" ) -func RandomString(n int) string { +func String(n int) string { bytes := make([]byte, n) if _, err := rand.Read(bytes); err != nil { return "" From 90d2dd560877919d67c750482d3bcc6a53545200 Mon Sep 17 00:00:00 2001 From: KostLinux Date: Tue, 18 Mar 2025 20:43:15 +0200 Subject: [PATCH 2/8] chore: add configurable ssl mode --- .env.example | 2 ++ configuration/config.go | 4 ++++ repository/connection.go | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 29968a8..516a829 100644 --- a/.env.example +++ b/.env.example @@ -12,3 +12,5 @@ POSTGRES_PORT="5432" POSTGRES_DB="postgres" POSTGRES_USER="postgres" POSTGRES_PASSWORD="postgres" +# POSTGRES_SSL_MODE="require" is needed for production cloud environments (e.g AWS Aurora, Azure PostgreSQL) +POSTGRES_SSL_MODE="disable" \ No newline at end of file diff --git a/configuration/config.go b/configuration/config.go index 74ff02f..723dce8 100644 --- a/configuration/config.go +++ b/configuration/config.go @@ -14,6 +14,7 @@ type Config struct { PostgresDB string PostgresUser string PostgresPassword string + PostgresSSLMode string } func Load() (*Config, error) { @@ -25,6 +26,8 @@ func Load() (*Config, error) { } } + viper.SetDefault("POSTGRES_SSL_MODE", "disable") + viper.AutomaticEnv() return &Config{ @@ -36,5 +39,6 @@ func Load() (*Config, error) { PostgresDB: viper.GetString("POSTGRES_DB"), PostgresUser: viper.GetString("POSTGRES_USER"), PostgresPassword: viper.GetString("POSTGRES_PASSWORD"), + PostgresSSLMode: viper.GetString("POSTGRES_SSL_MODE"), }, nil } diff --git a/repository/connection.go b/repository/connection.go index 1be247f..4547f46 100644 --- a/repository/connection.go +++ b/repository/connection.go @@ -15,12 +15,13 @@ type Connection struct { } func NewConnection(cfg *configuration.Config) *Connection { - psqlURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", + psqlURL := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", cfg.PostgresUser, cfg.PostgresPassword, cfg.PostgresHost, cfg.PostgresPort, cfg.PostgresDB, + cfg.PostgresSSLMode, ) db, err := sqlx.Open("postgres", psqlURL) From f7cabaab38a2e3a0aeb413e3d44b9c2efc30a846 Mon Sep 17 00:00:00 2001 From: KostLinux Date: Tue, 18 Mar 2025 20:51:47 +0200 Subject: [PATCH 3/8] fix: conflicts from changes --- .env.example | 2 ++ configuration/config.go | 2 ++ go.mod | 1 + 3 files changed, 5 insertions(+) diff --git a/.env.example b/.env.example index d4e1d46..937f193 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,8 @@ POSTGRES_PORT="5432" POSTGRES_DB="postgres" POSTGRES_USER="postgres" POSTGRES_PASSWORD="postgres" +# POSTGRES_SSL_MODE="require" is needed for production cloud environments (e.g AWS Aurora, Azure PostgreSQL) +POSTGRES_SSL_MODE="disable" # CORS ## Allowed Frontend Origins diff --git a/configuration/config.go b/configuration/config.go index 4715f43..77e8430 100644 --- a/configuration/config.go +++ b/configuration/config.go @@ -17,6 +17,7 @@ type Env struct { PostgresDB string PostgresUser string PostgresPassword string + PostgresSSLMode string AllowedOrigins []string } @@ -48,6 +49,7 @@ func Load() (*Env, error) { PostgresDB: viper.GetString("POSTGRES_DB"), PostgresUser: viper.GetString("POSTGRES_USER"), PostgresPassword: viper.GetString("POSTGRES_PASSWORD"), + PostgresSSLMode: viper.GetString("POSTGRES_SSL_MODE"), AllowedOrigins: origins, }, nil } diff --git a/go.mod b/go.mod index 5bff8cb..17d8949 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module go-api-template go 1.23.5 + require ( github.com/gin-gonic/gin v1.10.0 github.com/go-openapi/strfmt v0.23.0 From 222b8ee26b72894edc1ce4f1ee9586774f37063a Mon Sep 17 00:00:00 2001 From: KostLinux Date: Tue, 18 Mar 2025 20:53:44 +0200 Subject: [PATCH 4/8] fix: deprecated linter used --- .golangci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index 208c3d5..5778c72 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -46,7 +46,7 @@ linters: - revive - staticcheck - stylecheck - - tenv + - usetesting - typecheck - unconvert - unparam From e92fd0a6726b1151a2c9585c65465fabd96413c7 Mon Sep 17 00:00:00 2001 From: KostLinux Date: Sun, 23 Mar 2025 22:15:41 +0200 Subject: [PATCH 5/8] feature: add platform agnostic OTEL support --- .env.example | 8 ++ bin/load-test.js | 72 ++++++++++++++++++ configuration/{config.go => env.go} | 2 + configuration/telemetry.go | 43 +++++++++++ controller/user.go | 12 ++- go.mod | 34 +++++++-- go.sum | 81 ++++++++++++++------ main.go | 11 +++ middleware/router.go | 2 + pkg/telemetry/open.go | 113 ++++++++++++++++++++++++++++ repository/connection.go | 28 ++++++- repository/user.go | 33 +++++--- repository/user_mock.go | 7 +- repository/user_test.go | 8 +- service/user.go | 19 ++++- service/user_mock.go | 5 +- service/user_test.go | 12 +-- 17 files changed, 422 insertions(+), 68 deletions(-) create mode 100644 bin/load-test.js rename configuration/{config.go => env.go} (95%) create mode 100644 configuration/telemetry.go create mode 100644 pkg/telemetry/open.go diff --git a/.env.example b/.env.example index 937f193..70843f5 100644 --- a/.env.example +++ b/.env.example @@ -18,3 +18,11 @@ POSTGRES_SSL_MODE="disable" # CORS ## Allowed Frontend Origins ALLOWED_ORIGINS=http://localhost:3000,http://example.com + +# OTEL Configuration +## Example for New Relic +OTEL_SERVICE_NAME=my-service +OTEL_SERVICE_VERSION=1.0.0 +OTEL_EXPORTER_OTLP_ENDPOINT=otlp.nr-data.net:4317 +OTEL_EXPORTER_OTLP_HEADERS="api-key=eu01xxe4dab6145ea38a1dd4888e8138FFFFNRAL" +OTEL_INSECURE=true \ No newline at end of file diff --git a/bin/load-test.js b/bin/load-test.js new file mode 100644 index 0000000..049ea67 --- /dev/null +++ b/bin/load-test.js @@ -0,0 +1,72 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +export const options = { + stages: [ + { duration: '30s', target: 5 }, // Ramp up to 5 users + { duration: '1m', target: 5 }, // Stay at 5 users + { duration: '20s', target: 10 }, // Ramp up to 10 users + { duration: '1m', target: 10 }, // Stay at 10 users + { duration: '20s', target: 0 }, // Scale down to 0 users + ], + thresholds: { + http_req_duration: ['p(95)<2000'], // 95% of requests must complete below 2s + http_req_failed: ['rate<0.1'], // Less than 10% can fail + }, +}; + +const BASE_URL = 'http://localhost:8080/api/v1'; + +// Generates a random UUID v4 +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +export default function () { + const userId = uuidv4(); + const authToken = `Bearer ${randomString(32)}`; + + // Add trace context headers + const headers = { + 'Authorization': authToken, + 'traceparent': `00-${randomString(32)}-${randomString(16)}-01`, + 'Content-Type': 'application/json', + }; + + // Test user endpoint with trace context + const userResponse = http.get(`${BASE_URL}/users/${userId}`, { + headers: headers, + tags: { name: 'GetUserByID' }, + }); + + // Check response and add custom span attributes via tags + check(userResponse, { + 'status is 401 or 422': (r) => r.status === 401 || r.status === 422, + 'response time OK': (r) => r.timings.duration < 2000, + }); + + // Add some variation in the test pattern + if (Math.random() < 0.3) { + // Simulate slow requests occasionally + sleep(2); + } else { + sleep(1); + } + + // Test invalid UUID to generate error traces + if (Math.random() < 0.1) { + const invalidResponse = http.get(`${BASE_URL}/users/invalid-uuid`, { + headers: headers, + tags: { name: 'GetUserByIDInvalid' }, + }); + + check(invalidResponse, { + 'invalid uuid returns 400': (r) => r.status === 400, + }); + } +} \ No newline at end of file diff --git a/configuration/config.go b/configuration/env.go similarity index 95% rename from configuration/config.go rename to configuration/env.go index 77e8430..398a3be 100644 --- a/configuration/config.go +++ b/configuration/env.go @@ -19,6 +19,7 @@ type Env struct { PostgresPassword string PostgresSSLMode string AllowedOrigins []string + Telemetry *Telemetry } func Load() (*Env, error) { @@ -51,5 +52,6 @@ func Load() (*Env, error) { PostgresPassword: viper.GetString("POSTGRES_PASSWORD"), PostgresSSLMode: viper.GetString("POSTGRES_SSL_MODE"), AllowedOrigins: origins, + Telemetry: TelemetryNew(), }, nil } diff --git a/configuration/telemetry.go b/configuration/telemetry.go new file mode 100644 index 0000000..929e94d --- /dev/null +++ b/configuration/telemetry.go @@ -0,0 +1,43 @@ +package configuration + +import ( + "strings" + + "github.com/spf13/viper" +) + +type Telemetry struct { + ServiceName string + ServiceVersion string + Environment string + Endpoint string + Headers map[string]string + Insecure bool +} + +// TelemetryNew creates a new Telemetry configuration from environment +func TelemetryNew() *Telemetry { + viper.SetDefault("OTEL_SERVICE_NAME", "go-api-template") + viper.SetDefault("OTEL_SERVICE_VERSION", "1.0.0") + viper.SetDefault("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4317") + viper.SetDefault("OTEL_INSECURE", true) + + headers := make(map[string]string) + if headerStr := viper.GetString("OTEL_EXPORTER_OTLP_HEADERS"); headerStr != "" { + for _, header := range strings.Split(headerStr, ",") { + parts := strings.SplitN(header, "=", 2) + if len(parts) == 2 { + headers[parts[0]] = parts[1] + } + } + } + + return &Telemetry{ + ServiceName: viper.GetString("OTEL_SERVICE_NAME"), + ServiceVersion: viper.GetString("OTEL_SERVICE_VERSION"), + Environment: viper.GetString("APP_ENV"), + Endpoint: viper.GetString("OTEL_EXPORTER_OTLP_ENDPOINT"), + Headers: headers, + Insecure: viper.GetBool("OTEL_INSECURE"), + } +} diff --git a/controller/user.go b/controller/user.go index 2a2ed17..0465429 100644 --- a/controller/user.go +++ b/controller/user.go @@ -6,6 +6,8 @@ import ( "github.com/gin-gonic/gin" "github.com/go-openapi/strfmt" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" ) type IUser interface { @@ -14,11 +16,13 @@ type IUser interface { type user struct { service service.IUser + tracer trace.Tracer } func NewUser(service service.IUser) IUser { return &user{ service: service, + tracer: otel.Tracer("controller/user"), } } @@ -36,15 +40,19 @@ func NewUser(service service.IUser) IUser { // @Failure 500 {object} model.InternalErrorResponse // @Router /users/{user_id} [get] func (controller *user) UserByID(ctx *gin.Context) { - // Validate path params + spanCtx, span := controller.tracer.Start(ctx.Request.Context(), "UserByID") + defer span.End() + userID := ctx.Param("user_id") if !strfmt.IsUUID4(userID) { + span.RecordError(commonerrors.ErrInvalidUserID) StatusBadRequest(ctx, commonerrors.ErrInvalidUserID) return } - response, err := controller.service.UserByID(ctx, strfmt.UUID4(userID)) + response, err := controller.service.UserByID(spanCtx, strfmt.UUID4(userID)) if err != nil { + span.RecordError(err) HandleCommonErrors(ctx, err) return } diff --git a/go.mod b/go.mod index cf04756..a2041c9 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module go-api-template go 1.23.5 require ( + github.com/XSAM/otelsql v0.37.0 github.com/gin-gonic/gin v1.10.0 github.com/go-openapi/strfmt v0.23.0 github.com/go-playground/validator/v10 v10.25.0 @@ -16,7 +17,15 @@ require ( github.com/spf13/viper v1.20.0 github.com/stretchr/testify v1.10.0 github.com/swaggo/swag v1.16.4 + go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0 + go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 + go.opentelemetry.io/otel v1.35.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 + go.opentelemetry.io/otel/sdk v1.35.0 + go.opentelemetry.io/otel/trace v1.35.0 go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.71.0 gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 gorm.io/driver/postgres v1.5.11 ) @@ -26,14 +35,16 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/bytedance/sonic v1.11.6 // indirect - github.com/bytedance/sonic/loader v0.1.1 // indirect - github.com/cloudwego/base64x v0.1.4 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect + github.com/bytedance/sonic v1.12.10 // indirect + github.com/bytedance/sonic/loader v0.2.3 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/errors v0.22.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect @@ -43,6 +54,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect @@ -51,7 +63,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -71,15 +83,21 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/arch v0.8.0 // indirect + golang.org/x/arch v0.14.0 // indirect golang.org/x/crypto v0.35.0 // indirect golang.org/x/net v0.36.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - google.golang.org/protobuf v1.36.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/gorm v1.25.10 // indirect diff --git a/go.sum b/go.sum index 0baf45f..959e63b 100644 --- a/go.sum +++ b/go.sum @@ -6,15 +6,19 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/XSAM/otelsql v0.37.0 h1:ya5RNw028JW0eJW8Ma4AmoKxAYsJSGuNVbC7F1J457A= +github.com/XSAM/otelsql v0.37.0/go.mod h1:LHbCu49iU8p255nCn1oi04oX2UjSoRcUMiKEHo2a5qM= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= -github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic v1.12.10 h1:uVCQr6oS5669E9ZVW0HyksTLfNS7Q/9hV6IVS4nEMsI= +github.com/bytedance/sonic v1.12.10/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= -github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= +github.com/bytedance/sonic/loader v0.2.3/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/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -27,10 +31,15 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -59,11 +68,15 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -85,8 +98,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -126,8 +139,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= @@ -153,6 +166,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -165,15 +179,36 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +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/github.com/gin-gonic/gin/otelgin v0.60.0 h1:jj/B7eX95/mOxim9g9laNZkOHKz/XCHG0G410SntRy4= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.60.0/go.mod h1:ZvRTVaYYGypytG0zRp2A60lpj//cMq3ZnxYdZaljVBM= +go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0 h1:0NgN/3SYkqYJ9NBlDfl/2lzVlwos/YQLvi8sUrzJRBE= +go.opentelemetry.io/contrib/instrumentation/runtime v0.60.0/go.mod h1:oxpUfhTkhgQaYIjtBt3T3w135dLoxq//qo3WPlPIKkE= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= -golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= +golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= @@ -185,7 +220,6 @@ golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -197,8 +231,14 @@ golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -218,4 +258,3 @@ gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSk gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go index 17e188b..4ca401d 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "go-api-template/middleware" "go-api-template/model" "go-api-template/pkg/logger" + "go-api-template/pkg/telemetry" "go-api-template/repository" "go-api-template/service" "log" @@ -56,6 +57,16 @@ func main() { // Initialize the controllers controllers := controller.NewControllers(services) + // Initialize OpenTelemetry + otel := configuration.TelemetryNew() + otel.ServiceName = cfg.Telemetry.ServiceName + otel.Environment = cfg.Telemetry.Environment + otel.Endpoint = cfg.Telemetry.Endpoint + otel.Headers = cfg.Telemetry.Headers + + cleanup := telemetry.InitTracer(otel) + defer cleanup() + // Initialize router router, err := middleware.NewRouter(cfg, controllers) if err != nil { diff --git a/middleware/router.go b/middleware/router.go index 3af4931..87f6078 100644 --- a/middleware/router.go +++ b/middleware/router.go @@ -7,6 +7,7 @@ import ( "go-api-template/pkg/logger" "github.com/gin-gonic/gin" + "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" ) func NewRouter(cfg *configuration.Env, controllers *controller.Controllers) (*gin.Engine, error) { @@ -15,6 +16,7 @@ func NewRouter(cfg *configuration.Env, controllers *controller.Controllers) (*gi } router := gin.Default() + router.Use(otelgin.Middleware(cfg.Telemetry.ServiceName)) // Initialize middlewares middleware := NewMiddlewares(cfg) diff --git a/pkg/telemetry/open.go b/pkg/telemetry/open.go new file mode 100644 index 0000000..484d5e3 --- /dev/null +++ b/pkg/telemetry/open.go @@ -0,0 +1,113 @@ +package telemetry + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "go-api-template/configuration" + "go-api-template/pkg/logger" + "time" + + "go.opentelemetry.io/contrib/instrumentation/runtime" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" + "google.golang.org/grpc/credentials" +) + +func InitTracer(cfg *configuration.Telemetry) func() { + ctx := context.Background() + shutdown, err := setupOTelSDK(ctx, cfg) + if err != nil { + logger.Fatalf("failed to setup OpenTelemetry: %v", err) + } + + return func() { + logger.Infof("Shutting down OpenTelemetry") + if err := shutdown(context.Background()); err != nil { + logger.Errorf("failed to shutdown OpenTelemetry: %v", err) + } + } +} + +func setupOTelSDK(ctx context.Context, cfg *configuration.Telemetry) (func(context.Context) error, error) { + var shutdownFuncs []func(context.Context) error + + // Create shutdown function + shutdown := func(ctx context.Context) error { + var err error + for _, fn := range shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + shutdownFuncs = nil + return err + } + + // Create resource + res, err := resource.New(ctx, + resource.WithFromEnv(), + resource.WithSchemaURL(semconv.SchemaURL), + resource.WithAttributes( + semconv.ServiceNameKey.String(cfg.ServiceName), + semconv.ServiceVersionKey.String(cfg.ServiceVersion), + semconv.DeploymentEnvironmentKey.String(cfg.Environment), + ), + ) + if err != nil { + return nil, fmt.Errorf("failed to create resource: %w", err) + } + + // Configure OTLP exporter + opts := []otlptracegrpc.Option{ + otlptracegrpc.WithEndpoint(cfg.Endpoint), + otlptracegrpc.WithHeaders(cfg.Headers), + otlptracegrpc.WithTLSCredentials(credentials.NewTLS(&tls.Config{ + MinVersion: tls.VersionTLS12, + })), + otlptracegrpc.WithTimeout(30 * time.Second), + otlptracegrpc.WithCompressor("gzip"), + } + + client := otlptracegrpc.NewClient(opts...) + traceExporter, err := otlptrace.New(ctx, client) + if err != nil { + return nil, fmt.Errorf("failed to create trace exporter: %w", err) + } + + // Create BatchSpanProcessor + bsp := sdktrace.NewBatchSpanProcessor(traceExporter, + sdktrace.WithMaxQueueSize(2048), + sdktrace.WithBatchTimeout(2*time.Second), + sdktrace.WithMaxExportBatchSize(512), + ) + + // Create TracerProvider + tracerProvider := sdktrace.NewTracerProvider( + sdktrace.WithResource(res), + sdktrace.WithSpanProcessor(bsp), + sdktrace.WithSampler(sdktrace.AlwaysSample()), + ) + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + + // Set global TracerProvider + otel.SetTracerProvider(tracerProvider) + + // Set global Propagator + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + + // Enable runtime metrics + if err := runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second)); err != nil { + return nil, fmt.Errorf("failed to start runtime metrics: %w", err) + } + + logger.Infof("Successfully initialized OpenTelemetry") + return shutdown, nil +} diff --git a/repository/connection.go b/repository/connection.go index 0a81ea3..925bf95 100644 --- a/repository/connection.go +++ b/repository/connection.go @@ -1,12 +1,17 @@ package repository import ( + "context" "fmt" "go-api-template/configuration" "go-api-template/pkg/logger" + "github.com/XSAM/otelsql" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" // driver for PostgreSQL + "go.opentelemetry.io/otel" + + semconv "go.opentelemetry.io/otel/semconv/v1.24.0" ) type Connection struct { @@ -23,13 +28,28 @@ func NewConnection(cfg *configuration.Env) *Connection { cfg.PostgresSSLMode, ) - db, err := sqlx.Open("postgres", psqlURL) + // Create an instrumented sql.DB + sqlDB, err := otelsql.Open("postgres", psqlURL, + otelsql.WithAttributes( + semconv.DBSystemPostgreSQL, + semconv.DBNameKey.String(cfg.PostgresDB), + ), + otelsql.WithTracerProvider(otel.GetTracerProvider()), + otelsql.WithSpanOptions(otelsql.SpanOptions{ + Ping: true, + RowsNext: true, + }), + ) if err != nil { - logger.Fatalf("connecting to database failed. %v", err) + logger.Fatalf("failed to create database connection: %v", err) } - if err = db.Ping(); err != nil { - logger.Fatalf("connecting to database failed. %v", err) + // Wrap sql.DB with sqlx + db := sqlx.NewDb(sqlDB, "postgres") + + // Test the connection with context + if err = db.PingContext(context.Background()); err != nil { + logger.Fatalf("connecting to database failed: %v", err) } logger.Infof("database connection established") diff --git a/repository/user.go b/repository/user.go index e884aac..f4acf1b 100644 --- a/repository/user.go +++ b/repository/user.go @@ -1,6 +1,7 @@ package repository import ( + "context" "database/sql" "go-api-template/model/commonerrors" repositorymodel "go-api-template/repository/model" @@ -8,20 +9,25 @@ import ( "github.com/jmoiron/sqlx" "github.com/pkg/errors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) type IUser interface { Begin() (*sqlx.Tx, error) - SelectUserByFilter(filter repositorymodel.UsersFilter) (*repositorymodel.User, error) + SelectUserByFilter(ctx context.Context, filter repositorymodel.UsersFilter) (*repositorymodel.User, error) } type user struct { - db *sqlx.DB + db *sqlx.DB + tracer trace.Tracer } func NewUser(db *sqlx.DB) IUser { return &user{ - db: db, + db: db, + tracer: otel.Tracer("repository/user"), } } @@ -29,25 +35,28 @@ func (repository *user) Begin() (*sqlx.Tx, error) { return repository.db.Beginx() } -func (repository *user) SelectUserByFilter(filter repositorymodel.UsersFilter) (*repositorymodel.User, error) { +func (repository *user) SelectUserByFilter(ctx context.Context, filter repositorymodel.UsersFilter) (*repositorymodel.User, error) { + ctx, span := repository.tracer.Start(ctx, "SelectUserByFilter") + defer span.End() + var user repositorymodel.User whereCondition, args := buildUsersWhereCondition(filter) - query := ` - SELECT - * - FROM - users - ` + query := `SELECT * FROM users` if whereCondition != "" { query += " WHERE " + whereCondition } - if err := repository.db.Get(&user, query, args...); err != nil { + span.SetAttributes( + attribute.String("db.statement", query), + attribute.String("db.system", "postgresql"), + ) + + if err := repository.db.GetContext(ctx, &user, query, args...); err != nil { + span.RecordError(err) if errors.Is(err, sql.ErrNoRows) { return nil, commonerrors.ErrUserNotFound } - return nil, errors.Wrap(err, "selecting user by filter failed") } diff --git a/repository/user_mock.go b/repository/user_mock.go index 4f4bdfa..581ea32 100644 --- a/repository/user_mock.go +++ b/repository/user_mock.go @@ -1,6 +1,7 @@ package repository import ( + "context" repositorymodel "go-api-template/repository/model" "github.com/jmoiron/sqlx" @@ -18,15 +19,13 @@ func (mock *UserMock) Begin() (*sqlx.Tx, error) { if args.Get(0) == nil { return nil, args.Error(1) } - return args.Get(0).(*sqlx.Tx), args.Error(1) } -func (mock *UserMock) SelectUserByFilter(filter repositorymodel.UsersFilter) (*repositorymodel.User, error) { - args := mock.Called(filter) +func (mock *UserMock) SelectUserByFilter(ctx context.Context, filter repositorymodel.UsersFilter) (*repositorymodel.User, error) { + args := mock.Called(ctx, filter) if args.Get(0) == nil { return nil, args.Error(1) } - return args.Get(0).(*repositorymodel.User), args.Error(1) } diff --git a/repository/user_test.go b/repository/user_test.go index dc8663e..fef632b 100644 --- a/repository/user_test.go +++ b/repository/user_test.go @@ -1,6 +1,7 @@ package repository import ( + "context" "database/sql" "errors" "go-api-template/model/commonerrors" @@ -78,7 +79,7 @@ func (suite *UserTestSuite) Test_SelectUserByFilter_ReturnsError_InCaseOfSelectF suite.dbMock.ExpectQuery("SELECT").WillReturnError(errors.New("failed")) // Act - _, err := suite.repository.SelectUserByFilter(usersFilter) + _, err := suite.repository.SelectUserByFilter(context.Background(), usersFilter) // Assert suite.Error(err) @@ -93,7 +94,7 @@ func (suite *UserTestSuite) Test_SelectUserByFilter_ReturnsError_InCaseOfUserNot suite.dbMock.ExpectQuery("SELECT").WillReturnError(sql.ErrNoRows) // Act - _, err := suite.repository.SelectUserByFilter(usersFilter) + _, err := suite.repository.SelectUserByFilter(context.Background(), usersFilter) // Assert suite.Error(err) @@ -107,11 +108,10 @@ func (suite *UserTestSuite) Test_SelectUserByFilter_ReturnsUser_InCaseOfSuccess( } rows := sqlmock.NewRows([]string{"id"}).AddRow(string(suite.userID)) - suite.dbMock.ExpectQuery("SELECT").WillReturnRows(rows) // Act - user, err := suite.repository.SelectUserByFilter(usersFilter) + user, err := suite.repository.SelectUserByFilter(context.Background(), usersFilter) // Assert suite.NoError(err) diff --git a/service/user.go b/service/user.go index 75ed6bf..f7affbe 100644 --- a/service/user.go +++ b/service/user.go @@ -1,21 +1,25 @@ package service import ( + "context" "go-api-template/model" "go-api-template/repository" repositorymodel "go-api-template/repository/model" "go-api-template/service/mapper" - "github.com/gin-gonic/gin" "github.com/go-openapi/strfmt" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) type IUser interface { - UserByID(ctx *gin.Context, userID strfmt.UUID4) (*model.UserByIDResponse, error) + UserByID(ctx context.Context, userID strfmt.UUID4) (*model.UserByIDResponse, error) } type user struct { userRepository repository.IUser + tracer trace.Tracer } func NewUser( @@ -23,16 +27,23 @@ func NewUser( ) IUser { return &user{ userRepository: userRepository, + tracer: otel.Tracer("service/user"), } } -func (service *user) UserByID(_ *gin.Context, userID strfmt.UUID4) (*model.UserByIDResponse, error) { +func (service *user) UserByID(ctx context.Context, userID strfmt.UUID4) (*model.UserByIDResponse, error) { + ctx, span := service.tracer.Start(ctx, "UserByID") + defer span.End() + + span.SetAttributes(attribute.String("user.id", userID.String())) + filter := repositorymodel.UsersFilter{ ID: &userID, } - user, err := service.userRepository.SelectUserByFilter(filter) + user, err := service.userRepository.SelectUserByFilter(ctx, filter) if err != nil { + span.RecordError(err) return nil, err } diff --git a/service/user_mock.go b/service/user_mock.go index 2d6f7c2..b1a0e0c 100644 --- a/service/user_mock.go +++ b/service/user_mock.go @@ -1,9 +1,9 @@ package service import ( + "context" "go-api-template/model" - "github.com/gin-gonic/gin" "github.com/go-openapi/strfmt" "github.com/stretchr/testify/mock" ) @@ -14,11 +14,10 @@ type UserMock struct { var _ IUser = &UserMock{} -func (mock *UserMock) UserByID(ctx *gin.Context, userID strfmt.UUID4) (*model.UserByIDResponse, error) { +func (mock *UserMock) UserByID(ctx context.Context, userID strfmt.UUID4) (*model.UserByIDResponse, error) { args := mock.Called(ctx, userID) if args.Get(0) == nil { return nil, args.Error(1) } - return args.Get(0).(*model.UserByIDResponse), args.Error(1) } diff --git a/service/user_test.go b/service/user_test.go index 5911f3b..19f2fed 100644 --- a/service/user_test.go +++ b/service/user_test.go @@ -50,12 +50,12 @@ func (suite *UserTestSuite) Test_UserByID_ReturnsError_InCaseOfSelectUserByFilte } suite.userRepositoryMock. - On("SelectUserByFilter", filter). + On("SelectUserByFilter", suite.ctx.Request.Context(), filter). Return(nil, suite.failedError). Once() // Act - response, err := suite.service.UserByID(suite.ctx, suite.userID) + response, err := suite.service.UserByID(suite.ctx.Request.Context(), suite.userID) // Assert suite.Nil(response) @@ -71,12 +71,12 @@ func (suite *UserTestSuite) Test_UserByID_ReturnsError_InCaseOfUserNotFound() { } suite.userRepositoryMock. - On("SelectUserByFilter", filter). + On("SelectUserByFilter", suite.ctx.Request.Context(), filter). Return(nil, commonerrors.ErrUserNotFound). Once() // Act - response, err := suite.service.UserByID(suite.ctx, suite.userID) + response, err := suite.service.UserByID(suite.ctx.Request.Context(), suite.userID) // Assert suite.Nil(response) @@ -96,12 +96,12 @@ func (suite *UserTestSuite) Test_UserByID_ReturnsUser_InCaseOfSuccess() { } suite.userRepositoryMock. - On("SelectUserByFilter", filter). + On("SelectUserByFilter", suite.ctx.Request.Context(), filter). Return(user, nil). Once() // Act - response, err := suite.service.UserByID(suite.ctx, suite.userID) + response, err := suite.service.UserByID(suite.ctx.Request.Context(), suite.userID) // Assert suite.Nil(err) From 3298f7899d9ef2de9c20280066d1d329d641cfb3 Mon Sep 17 00:00:00 2001 From: KostLinux Date: Sun, 23 Mar 2025 22:38:47 +0200 Subject: [PATCH 6/8] fix: some idiom fixes for collector --- .env.example | 8 +++-- configuration/telemetry.go | 34 ++++++++++++------- pkg/telemetry/{open.go => collector.go} | 45 ++++++++++--------------- pkg/telemetry/shutdown.go | 37 ++++++++++++++++++++ 4 files changed, 82 insertions(+), 42 deletions(-) rename pkg/telemetry/{open.go => collector.go} (69%) create mode 100644 pkg/telemetry/shutdown.go diff --git a/.env.example b/.env.example index 70843f5..e984096 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,8 @@ ALLOWED_ORIGINS=http://localhost:3000,http://example.com ## Example for New Relic OTEL_SERVICE_NAME=my-service OTEL_SERVICE_VERSION=1.0.0 -OTEL_EXPORTER_OTLP_ENDPOINT=otlp.nr-data.net:4317 -OTEL_EXPORTER_OTLP_HEADERS="api-key=eu01xxe4dab6145ea38a1dd4888e8138FFFFNRAL" -OTEL_INSECURE=true \ No newline at end of file +OTEL_EXPORTER_OTLP_ENDPOINT=otlp.eu01.nr-data.net:4317 +OTEL_EXPORTER_OTLP_HEADERS="api-key=XXXX" +OTEL_INSECURE=false +OTEL_EXPORTER_OTLP_COMPRESSION=gzip +OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT=4094 \ No newline at end of file diff --git a/configuration/telemetry.go b/configuration/telemetry.go index 929e94d..96f21f6 100644 --- a/configuration/telemetry.go +++ b/configuration/telemetry.go @@ -7,12 +7,15 @@ import ( ) type Telemetry struct { - ServiceName string - ServiceVersion string - Environment string - Endpoint string - Headers map[string]string - Insecure bool + ServiceName string + ServiceVersion string + Environment string + Endpoint string + Headers map[string]string + Insecure bool + QueueSize int + MaxExportBatchSize int + Compression string } // TelemetryNew creates a new Telemetry configuration from environment @@ -21,6 +24,10 @@ func TelemetryNew() *Telemetry { viper.SetDefault("OTEL_SERVICE_VERSION", "1.0.0") viper.SetDefault("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4317") viper.SetDefault("OTEL_INSECURE", true) + viper.SetDefault("OTEL_EXPORTER_OTLP_HEADERS", "") + viper.SetDefault("OTEL_EXPORTER_OTLP_QUEUE_SIZE", 4096) + viper.SetDefault("OTEL_EXPORTER_OTLP_MAX_EXPORT_BATCH_SIZE", 512) + viper.SetDefault("OTEL_EXPORTER_OTLP_COMPRESSION", "gzip") headers := make(map[string]string) if headerStr := viper.GetString("OTEL_EXPORTER_OTLP_HEADERS"); headerStr != "" { @@ -33,11 +40,14 @@ func TelemetryNew() *Telemetry { } return &Telemetry{ - ServiceName: viper.GetString("OTEL_SERVICE_NAME"), - ServiceVersion: viper.GetString("OTEL_SERVICE_VERSION"), - Environment: viper.GetString("APP_ENV"), - Endpoint: viper.GetString("OTEL_EXPORTER_OTLP_ENDPOINT"), - Headers: headers, - Insecure: viper.GetBool("OTEL_INSECURE"), + ServiceName: viper.GetString("OTEL_SERVICE_NAME"), + ServiceVersion: viper.GetString("OTEL_SERVICE_VERSION"), + Environment: viper.GetString("APP_ENV"), + Endpoint: viper.GetString("OTEL_EXPORTER_OTLP_ENDPOINT"), + Headers: headers, + Insecure: viper.GetBool("OTEL_INSECURE"), + QueueSize: viper.GetInt("OTEL_EXPORTER_OTLP_QUEUE_SIZE"), + MaxExportBatchSize: viper.GetInt("OTEL_EXPORTER_OTLP_MAX_EXPORT_BATCH_SIZE"), + Compression: viper.GetString("OTEL_EXPORTER_OTLP_COMPRESSION"), } } diff --git a/pkg/telemetry/open.go b/pkg/telemetry/collector.go similarity index 69% rename from pkg/telemetry/open.go rename to pkg/telemetry/collector.go index 484d5e3..4a6906b 100644 --- a/pkg/telemetry/open.go +++ b/pkg/telemetry/collector.go @@ -3,7 +3,6 @@ package telemetry import ( "context" "crypto/tls" - "errors" "fmt" "go-api-template/configuration" "go-api-template/pkg/logger" @@ -22,7 +21,7 @@ import ( func InitTracer(cfg *configuration.Telemetry) func() { ctx := context.Background() - shutdown, err := setupOTelSDK(ctx, cfg) + shutdown, err := newOtelCollector(ctx, cfg) if err != nil { logger.Fatalf("failed to setup OpenTelemetry: %v", err) } @@ -35,21 +34,14 @@ func InitTracer(cfg *configuration.Telemetry) func() { } } -func setupOTelSDK(ctx context.Context, cfg *configuration.Telemetry) (func(context.Context) error, error) { - var shutdownFuncs []func(context.Context) error +func newOtelCollector(ctx context.Context, cfg *configuration.Telemetry) (func(context.Context) error, error) { + // Cleanup functions which need to be executed when the OpenTelemetry SDK is shutting down. + // When shutdown is called (typically when application is terminating), + // it executes all these cleanup functions in order and combines their errors + shutdownHandler := NewShutdownHandler() - // Create shutdown function - shutdown := func(ctx context.Context) error { - var err error - for _, fn := range shutdownFuncs { - err = errors.Join(err, fn(ctx)) - } - shutdownFuncs = nil - return err - } - - // Create resource - res, err := resource.New(ctx, + // Setup OpenTelemetry SDK + resources, err := resource.New(ctx, resource.WithFromEnv(), resource.WithSchemaURL(semconv.SchemaURL), resource.WithAttributes( @@ -59,10 +51,10 @@ func setupOTelSDK(ctx context.Context, cfg *configuration.Telemetry) (func(conte ), ) if err != nil { - return nil, fmt.Errorf("failed to create resource: %w", err) + return nil, fmt.Errorf("failed to create Otel SDK resource: %w", err) } - // Configure OTLP exporter + // Configure OTEL Exporter opts := []otlptracegrpc.Option{ otlptracegrpc.WithEndpoint(cfg.Endpoint), otlptracegrpc.WithHeaders(cfg.Headers), @@ -70,7 +62,7 @@ func setupOTelSDK(ctx context.Context, cfg *configuration.Telemetry) (func(conte MinVersion: tls.VersionTLS12, })), otlptracegrpc.WithTimeout(30 * time.Second), - otlptracegrpc.WithCompressor("gzip"), + otlptracegrpc.WithCompressor(cfg.Compression), } client := otlptracegrpc.NewClient(opts...) @@ -80,19 +72,18 @@ func setupOTelSDK(ctx context.Context, cfg *configuration.Telemetry) (func(conte } // Create BatchSpanProcessor - bsp := sdktrace.NewBatchSpanProcessor(traceExporter, - sdktrace.WithMaxQueueSize(2048), + batchSpanProcessor := sdktrace.NewBatchSpanProcessor(traceExporter, + sdktrace.WithMaxQueueSize(cfg.QueueSize), sdktrace.WithBatchTimeout(2*time.Second), - sdktrace.WithMaxExportBatchSize(512), + sdktrace.WithMaxExportBatchSize(cfg.MaxExportBatchSize), ) - // Create TracerProvider tracerProvider := sdktrace.NewTracerProvider( - sdktrace.WithResource(res), - sdktrace.WithSpanProcessor(bsp), + sdktrace.WithResource(resources), + sdktrace.WithSpanProcessor(batchSpanProcessor), sdktrace.WithSampler(sdktrace.AlwaysSample()), ) - shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + shutdownHandler.AddFunction(tracerProvider.Shutdown) // Set global TracerProvider otel.SetTracerProvider(tracerProvider) @@ -109,5 +100,5 @@ func setupOTelSDK(ctx context.Context, cfg *configuration.Telemetry) (func(conte } logger.Infof("Successfully initialized OpenTelemetry") - return shutdown, nil + return shutdownHandler.Shutdown, nil } diff --git a/pkg/telemetry/shutdown.go b/pkg/telemetry/shutdown.go new file mode 100644 index 0000000..d338f92 --- /dev/null +++ b/pkg/telemetry/shutdown.go @@ -0,0 +1,37 @@ +package telemetry + +import ( + ctx "context" + "errors" + "go-api-template/pkg/logger" +) + +type ShutdownHandler struct { + funcs []func(ctx.Context) error +} + +// NewShutdownHandler creates a new shutdown handler +func NewShutdownHandler() *ShutdownHandler { + return &ShutdownHandler{ + funcs: make([]func(ctx.Context) error, 0), + } +} + +// AddFunction adds a shutdown function to the handler +func (handle *ShutdownHandler) AddFunction(function func(ctx.Context) error) { + handle.funcs = append(handle.funcs, function) +} + +// Shutdown executes all registered shutdown functions +func (handle *ShutdownHandler) Shutdown(ctx ctx.Context) error { + var err error + for _, functions := range handle.funcs { + if ferr := functions(ctx); ferr != nil { + err = errors.Join(err, ferr) + logger.Errorf("shutdown function failed: %v", ferr) + } + } + + handle.funcs = nil + return err +} From 448001b980527349e9295381b31b0a2310e03800 Mon Sep 17 00:00:00 2001 From: KostLinux Date: Mon, 24 Mar 2025 08:28:36 +0200 Subject: [PATCH 7/8] feature: add possibility to disable otel --- .env.example | 10 ++++++++-- configuration/telemetry.go | 7 +++++-- main.go | 18 ++++++++++-------- middleware/router.go | 4 +++- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index e984096..bd2c7e7 100644 --- a/.env.example +++ b/.env.example @@ -21,10 +21,16 @@ ALLOWED_ORIGINS=http://localhost:3000,http://example.com # OTEL Configuration ## Example for New Relic +OTEL_ENABLED=false + +## Application name which will be shown in OTEL Frontend (e.g NewRelic, Dynatrace) OTEL_SERVICE_NAME=my-service OTEL_SERVICE_VERSION=1.0.0 + +## OTEL Exporter Configuration OTEL_EXPORTER_OTLP_ENDPOINT=otlp.eu01.nr-data.net:4317 OTEL_EXPORTER_OTLP_HEADERS="api-key=XXXX" -OTEL_INSECURE=false OTEL_EXPORTER_OTLP_COMPRESSION=gzip -OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT=4094 \ No newline at end of file +OTEL_EXPORTER_OTLP_QUEUE_SIZE=4094 +OTEL_EXPORTER_OTLP_MAX_EXPORT_BATCH_SIZE=1024 +OTEL_INSECURE=false diff --git a/configuration/telemetry.go b/configuration/telemetry.go index 96f21f6..00a1fb8 100644 --- a/configuration/telemetry.go +++ b/configuration/telemetry.go @@ -7,8 +7,7 @@ import ( ) type Telemetry struct { - ServiceName string - ServiceVersion string + Enabled bool Environment string Endpoint string Headers map[string]string @@ -16,10 +15,13 @@ type Telemetry struct { QueueSize int MaxExportBatchSize int Compression string + ServiceName string + ServiceVersion string } // TelemetryNew creates a new Telemetry configuration from environment func TelemetryNew() *Telemetry { + viper.SetDefault("OTEL_ENABLED", false) viper.SetDefault("OTEL_SERVICE_NAME", "go-api-template") viper.SetDefault("OTEL_SERVICE_VERSION", "1.0.0") viper.SetDefault("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4317") @@ -40,6 +42,7 @@ func TelemetryNew() *Telemetry { } return &Telemetry{ + Enabled: viper.GetBool("OTEL_ENABLED"), ServiceName: viper.GetString("OTEL_SERVICE_NAME"), ServiceVersion: viper.GetString("OTEL_SERVICE_VERSION"), Environment: viper.GetString("APP_ENV"), diff --git a/main.go b/main.go index 4ca401d..415bc44 100644 --- a/main.go +++ b/main.go @@ -58,14 +58,16 @@ func main() { controllers := controller.NewControllers(services) // Initialize OpenTelemetry - otel := configuration.TelemetryNew() - otel.ServiceName = cfg.Telemetry.ServiceName - otel.Environment = cfg.Telemetry.Environment - otel.Endpoint = cfg.Telemetry.Endpoint - otel.Headers = cfg.Telemetry.Headers - - cleanup := telemetry.InitTracer(otel) - defer cleanup() + if cfg.Telemetry.Enabled { + otel := configuration.TelemetryNew() + otel.ServiceName = cfg.Telemetry.ServiceName + otel.Environment = cfg.Telemetry.Environment + otel.Endpoint = cfg.Telemetry.Endpoint + otel.Headers = cfg.Telemetry.Headers + + cleanup := telemetry.InitTracer(otel) + defer cleanup() + } // Initialize router router, err := middleware.NewRouter(cfg, controllers) diff --git a/middleware/router.go b/middleware/router.go index 87f6078..cfa17ba 100644 --- a/middleware/router.go +++ b/middleware/router.go @@ -16,7 +16,9 @@ func NewRouter(cfg *configuration.Env, controllers *controller.Controllers) (*gi } router := gin.Default() - router.Use(otelgin.Middleware(cfg.Telemetry.ServiceName)) + if cfg.Telemetry.Enabled { + router.Use(otelgin.Middleware(cfg.Telemetry.ServiceName)) + } // Initialize middlewares middleware := NewMiddlewares(cfg) From ca963fb70adf099bb3bb8165996054b29a97432b Mon Sep 17 00:00:00 2001 From: KostLinux Date: Mon, 24 Mar 2025 08:29:45 +0200 Subject: [PATCH 8/8] feature: introduce otel in readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 377a171..989c67a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This is a template for a Go API project. It includes the following features: - API Security - API Logging - API Testing +- Platform agnostic OpenTelemetry ## Requirements