diff --git a/charts/lfx-v2-access-check/templates/deployment.yaml b/charts/lfx-v2-access-check/templates/deployment.yaml index 8604446..9e519a6 100644 --- a/charts/lfx-v2-access-check/templates/deployment.yaml +++ b/charts/lfx-v2-access-check/templates/deployment.yaml @@ -38,6 +38,30 @@ spec: value: "{{ .Values.nats.url }}" - name: JWKS_URL value: "{{ .Values.heimdall.jwks_url }}" + {{- if .Values.otel.serviceName }} + - name: OTEL_SERVICE_NAME + value: {{ .Values.otel.serviceName | quote }} + {{- end }} + {{- if .Values.otel.serviceVersion }} + - name: OTEL_SERVICE_VERSION + value: {{ .Values.otel.serviceVersion | quote }} + {{- end }} + {{- if .Values.otel.endpoint }} + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ .Values.otel.endpoint | quote }} + {{- end }} + {{- if .Values.otel.protocol }} + - name: OTEL_EXPORTER_OTLP_PROTOCOL + value: {{ .Values.otel.protocol | quote }} + {{- end }} + {{- if .Values.otel.insecure }} + - name: OTEL_EXPORTER_OTLP_INSECURE + value: {{ .Values.otel.insecure | quote }} + {{- end }} + {{- if .Values.otel.tracesExporter }} + - name: OTEL_TRACES_EXPORTER + value: {{ .Values.otel.tracesExporter | quote }} + {{- end }} {{- if .Values.app.resources }} resources: {{- toYaml .Values.app.resources | nindent 12 }} diff --git a/charts/lfx-v2-access-check/values.yaml b/charts/lfx-v2-access-check/values.yaml index 6d7c8b6..f4dacc5 100644 --- a/charts/lfx-v2-access-check/values.yaml +++ b/charts/lfx-v2-access-check/values.yaml @@ -50,3 +50,24 @@ heimdall: enabled: true url: http://lfx-platform-heimdall.lfx.svc.cluster.local:4456 jwks_url: http://lfx-platform-heimdall:4457/.well-known/jwks + +# otel is the configuration for OpenTelemetry tracing +otel: + # serviceName is the service name for OpenTelemetry resource identification + # (default: "lfx-v2-access-check") + serviceName: "" + # serviceVersion is the service version for OpenTelemetry resource identification + # (default: "1.0.0") + serviceVersion: "" + # protocol specifies the OTLP protocol: "grpc" or "http" + # (default: "grpc") + protocol: "grpc" + # endpoint is the OTLP collector endpoint + # For gRPC: typically "host:4317", for HTTP: typically "host:4318" + endpoint: "" + # insecure disables TLS for the OTLP connection + # Set to "true" for in-cluster communication without TLS + insecure: "false" + # tracesExporter specifies the traces exporter: "otlp" or "none" + # (default: "none") + tracesExporter: "none" diff --git a/cmd/lfx-access-check/main.go b/cmd/lfx-access-check/main.go index ccc3ac6..033a2e9 100644 --- a/cmd/lfx-access-check/main.go +++ b/cmd/lfx-access-check/main.go @@ -13,6 +13,7 @@ import ( "github.com/linuxfoundation/lfx-v2-access-check/internal/infrastructure/config" "github.com/linuxfoundation/lfx-v2-access-check/pkg/log" + "github.com/linuxfoundation/lfx-v2-access-check/pkg/utils" ) func init() { @@ -24,11 +25,25 @@ func main() { // Load configuration with CLI flags and environment variables cfg := config.LoadConfig() + // Set up OpenTelemetry SDK. + ctx := context.Background() + otelConfig := utils.OTelConfigFromEnv() + otelShutdown, err := utils.SetupOTelSDKWithConfig(ctx, otelConfig) + if err != nil { + slog.Error("error setting up OpenTelemetry SDK", "error", err) + os.Exit(1) + } + defer func() { + if shutdownErr := otelShutdown(context.Background()); shutdownErr != nil { + slog.Error("error shutting down OpenTelemetry SDK", "error", shutdownErr) + } + }() + // Setup signal handling for graceful shutdown sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(ctx) defer cancel() go func() { diff --git a/cmd/lfx-access-check/server.go b/cmd/lfx-access-check/server.go index f327c11..b300f82 100644 --- a/cmd/lfx-access-check/server.go +++ b/cmd/lfx-access-check/server.go @@ -18,6 +18,7 @@ import ( "github.com/linuxfoundation/lfx-v2-access-check/internal/middleware" "github.com/linuxfoundation/lfx-v2-access-check/pkg/constants" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "goa.design/clue/debug" goahttp "goa.design/goa/v3/http" ) @@ -97,6 +98,9 @@ func handleHTTPServer(ctx context.Context, cfg *config.Config, endpoints *access if cfg.Debug { handler = debug.HTTP()(handler) } + + // Wrap the handler with OpenTelemetry instrumentation + handler = otelhttp.NewHandler(handler, "access-check") } // Create HTTP server diff --git a/go.mod b/go.mod index c883024..70bf873 100644 --- a/go.mod +++ b/go.mod @@ -8,22 +8,32 @@ go 1.24.0 require ( github.com/auth0/go-jwt-middleware/v2 v2.2.2 github.com/nats-io/nats.go v1.37.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 + go.opentelemetry.io/otel/sdk v1.39.0 goa.design/goa/v3 v3.21.5 ) +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.6.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect +) + require ( github.com/aws/smithy-go v1.22.3 // indirect - github.com/go-logr/logr v1.4.3 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/term v0.32.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/grpc v1.73.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 // indirect github.com/go-chi/chi/v5 v5.2.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/gohugoio/hashstructure v0.5.0 // indirect github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 // indirect @@ -32,14 +42,21 @@ require ( github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/trace v1.39.0 // indirect goa.design/clue v1.2.1 golang.org/x/crypto v0.39.0 // indirect golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.26.0 // indirect golang.org/x/tools v0.34.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect + google.golang.org/grpc v1.73.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 01e7bdc..f69c983 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,19 @@ github.com/auth0/go-jwt-middleware/v2 v2.2.2 h1:vrvkFZf72r3Qbt45KLjBG3/6Xq2r3NTi github.com/auth0/go-jwt-middleware/v2 v2.2.2/go.mod h1:4vwxpVtu/Kl4c4HskT+gFLjq0dra8F1joxzamrje6J0= github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZsmLR/+RGffQSXwEkXgfLSA08qDn9AI= github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598/go.mod h1:0FpDmbrt36utu8jEmeU05dPC9AB5tsLYVVi+ZHfyuwI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +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= @@ -24,8 +29,14 @@ 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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d h1:Zj+PHjnhRYWBK6RqCDBcAhLXoi3TzC27Zad/Vn+gnVQ= github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d/go.mod h1:WZy8Q5coAB1zhY9AOBJP0O6J4BuDfbupUDavKY+I3+s= github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b h1:3E44bLeN8uKYdfQqVQycPnaVviZdBLbizFhU49mtbe4= @@ -38,22 +49,36 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -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/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= +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/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= +go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= goa.design/clue v1.2.1 h1:qFKQsNUzfwuBcTFZprfzOxipLTMiFvOLafuGm2Rnfh0= goa.design/clue v1.2.1/go.mod h1:Rn5RrcXYkqYNaActhTIcP76kchefNb0quA2or11ay9Q= goa.design/goa/v3 v3.21.5 h1:eS6SHJ1KZ5q5bhT/llw0LMTCWbosBwlFX4v8MctYs38= @@ -66,22 +91,25 @@ golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto= google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/utils/otel.go b/pkg/utils/otel.go new file mode 100644 index 0000000..09c4a08 --- /dev/null +++ b/pkg/utils/otel.go @@ -0,0 +1,170 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "context" + "errors" + "os" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" +) + +// OTelConfig holds OpenTelemetry configuration. +type OTelConfig struct { + ServiceName string + ServiceVersion string + Endpoint string + Protocol string // "grpc" or "http" + TracesExporter string // "otlp" or "none" + InsecureExporter bool +} + +// OTelConfigFromEnv creates an OTelConfig from environment variables. +// This follows the OpenTelemetry environment variable conventions. +func OTelConfigFromEnv() OTelConfig { + serviceName := os.Getenv("OTEL_SERVICE_NAME") + if serviceName == "" { + serviceName = "lfx-v2-access-check" + } + + serviceVersion := os.Getenv("OTEL_SERVICE_VERSION") + if serviceVersion == "" { + serviceVersion = "1.0.0" + } + + endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + if endpoint == "" { + endpoint = "localhost:4317" + } + + protocol := os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") + if protocol == "" { + protocol = "grpc" + } + + tracesExporter := os.Getenv("OTEL_TRACES_EXPORTER") + if tracesExporter == "" { + tracesExporter = "none" + } + + insecure := os.Getenv("OTEL_EXPORTER_OTLP_INSECURE") == "true" + + return OTelConfig{ + ServiceName: serviceName, + ServiceVersion: serviceVersion, + Endpoint: endpoint, + Protocol: protocol, + TracesExporter: tracesExporter, + InsecureExporter: insecure, + } +} + +// SetupOTelSDK bootstraps the OpenTelemetry pipeline using environment variables. +// If it does not return an error, make sure to call shutdown for proper cleanup. +func SetupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) { + return SetupOTelSDKWithConfig(ctx, OTelConfigFromEnv()) +} + +// SetupOTelSDKWithConfig bootstraps the OpenTelemetry pipeline with explicit configuration. +// If it does not return an error, make sure to call shutdown for proper cleanup. +func SetupOTelSDKWithConfig(ctx context.Context, config OTelConfig) (shutdown func(context.Context) error, err error) { + var shutdownFuncs []func(context.Context) error + + // shutdown calls cleanup functions registered via shutdownFuncs. + // The errors from the calls are joined. + // Each registered cleanup will be invoked once. + shutdown = func(ctx context.Context) error { + var err error + for _, fn := range shutdownFuncs { + err = errors.Join(err, fn(ctx)) + } + shutdownFuncs = nil + return err + } + + // handleErr calls shutdown for cleanup and makes sure that all errors are returned. + handleErr := func(inErr error) { + err = errors.Join(inErr, shutdown(ctx)) + } + + // Set up propagator. + prop := newPropagator() + otel.SetTextMapPropagator(prop) + + // Set up trace provider (only if exporter is configured). + if config.TracesExporter == "otlp" { + tracerProvider, tpErr := newTraceProvider(ctx, config) + if tpErr != nil { + handleErr(tpErr) + return + } + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + } + + return shutdown, nil +} + +func newPropagator() propagation.TextMapPropagator { + return propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) +} + +func newTraceProvider(ctx context.Context, config OTelConfig) (*trace.TracerProvider, error) { + var traceClient otlptrace.Client + + if config.Protocol == "http" { + opts := []otlptracehttp.Option{ + otlptracehttp.WithEndpoint(config.Endpoint), + } + if config.InsecureExporter { + opts = append(opts, otlptracehttp.WithInsecure()) + } + traceClient = otlptracehttp.NewClient(opts...) + } else { + opts := []otlptracegrpc.Option{ + otlptracegrpc.WithEndpoint(config.Endpoint), + } + if config.InsecureExporter { + opts = append(opts, otlptracegrpc.WithInsecure()) + } + traceClient = otlptracegrpc.NewClient(opts...) + } + + traceExporter, err := otlptrace.New(ctx, traceClient) + if err != nil { + return nil, err + } + + res, err := resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(config.ServiceName), + semconv.ServiceVersion(config.ServiceVersion), + ), + ) + if err != nil { + return nil, err + } + + traceProvider := trace.NewTracerProvider( + trace.WithBatcher(traceExporter, + trace.WithBatchTimeout(time.Second*5), + ), + trace.WithResource(res), + ) + return traceProvider, nil +} diff --git a/pkg/utils/otel_test.go b/pkg/utils/otel_test.go new file mode 100644 index 0000000..0562fbd --- /dev/null +++ b/pkg/utils/otel_test.go @@ -0,0 +1,135 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "context" + "os" + "testing" +) + +func TestOTelConfigFromEnv(t *testing.T) { + tests := []struct { + name string + envVars map[string]string + expected OTelConfig + }{ + { + name: "default values", + envVars: map[string]string{}, + expected: OTelConfig{ + ServiceName: "lfx-v2-access-check", + ServiceVersion: "1.0.0", + Endpoint: "localhost:4317", + Protocol: "grpc", + TracesExporter: "none", + InsecureExporter: false, + }, + }, + { + name: "custom values", + envVars: map[string]string{ + "OTEL_SERVICE_NAME": "custom-service", + "OTEL_SERVICE_VERSION": "2.0.0", + "OTEL_EXPORTER_OTLP_ENDPOINT": "otel-collector:4317", + "OTEL_EXPORTER_OTLP_PROTOCOL": "http", + "OTEL_TRACES_EXPORTER": "otlp", + "OTEL_EXPORTER_OTLP_INSECURE": "true", + }, + expected: OTelConfig{ + ServiceName: "custom-service", + ServiceVersion: "2.0.0", + Endpoint: "otel-collector:4317", + Protocol: "http", + TracesExporter: "otlp", + InsecureExporter: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Clear relevant env vars + envKeys := []string{ + "OTEL_SERVICE_NAME", + "OTEL_SERVICE_VERSION", + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_PROTOCOL", + "OTEL_TRACES_EXPORTER", + "OTEL_EXPORTER_OTLP_INSECURE", + } + for _, key := range envKeys { + os.Unsetenv(key) + } + + // Set test env vars + for key, value := range tt.envVars { + os.Setenv(key, value) + } + + config := OTelConfigFromEnv() + + if config.ServiceName != tt.expected.ServiceName { + t.Errorf("ServiceName = %v, want %v", config.ServiceName, tt.expected.ServiceName) + } + if config.ServiceVersion != tt.expected.ServiceVersion { + t.Errorf("ServiceVersion = %v, want %v", config.ServiceVersion, tt.expected.ServiceVersion) + } + if config.Endpoint != tt.expected.Endpoint { + t.Errorf("Endpoint = %v, want %v", config.Endpoint, tt.expected.Endpoint) + } + if config.Protocol != tt.expected.Protocol { + t.Errorf("Protocol = %v, want %v", config.Protocol, tt.expected.Protocol) + } + if config.TracesExporter != tt.expected.TracesExporter { + t.Errorf("TracesExporter = %v, want %v", config.TracesExporter, tt.expected.TracesExporter) + } + if config.InsecureExporter != tt.expected.InsecureExporter { + t.Errorf("InsecureExporter = %v, want %v", config.InsecureExporter, tt.expected.InsecureExporter) + } + }) + } +} + +func TestSetupOTelSDKWithConfig_NoExporter(t *testing.T) { + config := OTelConfig{ + ServiceName: "test-service", + TracesExporter: "none", + } + + ctx := context.Background() + shutdown, err := SetupOTelSDKWithConfig(ctx, config) + if err != nil { + t.Fatalf("SetupOTelSDKWithConfig() error = %v", err) + } + + if shutdown == nil { + t.Fatal("SetupOTelSDKWithConfig() shutdown function is nil") + } + + // Test shutdown + if err := shutdown(ctx); err != nil { + t.Errorf("shutdown() error = %v", err) + } +} + +func TestSetupOTelSDK(t *testing.T) { + // Clear env vars to use defaults + os.Unsetenv("OTEL_TRACES_EXPORTER") + + ctx := context.Background() + shutdown, err := SetupOTelSDK(ctx) + if err != nil { + t.Fatalf("SetupOTelSDK() error = %v", err) + } + + if shutdown == nil { + t.Fatal("SetupOTelSDK() shutdown function is nil") + } + + // Test shutdown + if err := shutdown(ctx); err != nil { + t.Errorf("shutdown() error = %v", err) + } +}