diff --git a/.github/workflows/ko-build-branch.yaml b/.github/workflows/ko-build-branch.yaml index aee3880..7004895 100644 --- a/.github/workflows/ko-build-branch.yaml +++ b/.github/workflows/ko-build-branch.yaml @@ -35,7 +35,14 @@ jobs: container_tag=$(echo "$HEAD_REF" | sed 's/[^_0-9a-zA-Z]/-/g' | cut -c -127) echo tag="$container_tag" >> "$GITHUB_OUTPUT" - name: Build auth-service for PR + env: + VERSION: ${{ steps.container_tag.outputs.tag }} + GIT_COMMIT: ${{ github.sha }} run: | + BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') + export BUILD_TIME + GIT_COMMIT=${GIT_COMMIT:0:7} + export GIT_COMMIT ko build github.com/linuxfoundation/lfx-v2-auth-service/cmd/server \ -B \ --platform linux/amd64,linux/arm64 \ diff --git a/.github/workflows/ko-build-main.yaml b/.github/workflows/ko-build-main.yaml index 3d5db67..5a3ca8f 100644 --- a/.github/workflows/ko-build-main.yaml +++ b/.github/workflows/ko-build-main.yaml @@ -27,7 +27,15 @@ jobs: - uses: ko-build/setup-ko@v0.8 with: version: v0.17.1 - - run: | + - name: Build and publish auth-service image + env: + VERSION: development + GIT_COMMIT: ${{ github.sha }} + run: | + BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') + export BUILD_TIME + GIT_COMMIT=${GIT_COMMIT:0:7} + export GIT_COMMIT ko build github.com/linuxfoundation/lfx-v2-auth-service/cmd/server \ -B \ --platform linux/amd64,linux/arm64 \ diff --git a/.github/workflows/ko-build-tag.yaml b/.github/workflows/ko-build-tag.yaml index dd7be5f..663f56f 100644 --- a/.github/workflows/ko-build-tag.yaml +++ b/.github/workflows/ko-build-tag.yaml @@ -54,7 +54,14 @@ jobs: version: v0.17.1 - name: Build and publish auth-service image + env: + VERSION: ${{ steps.prepare.outputs.app_version }} + GIT_COMMIT: ${{ github.sha }} run: | + BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') + export BUILD_TIME + GIT_COMMIT=${GIT_COMMIT:0:7} + export GIT_COMMIT ko build github.com/linuxfoundation/lfx-v2-auth-service/cmd/server \ -B \ --platform linux/amd64,linux/arm64 \ diff --git a/.ko.yaml b/.ko.yaml new file mode 100644 index 0000000..d1756c3 --- /dev/null +++ b/.ko.yaml @@ -0,0 +1,9 @@ +# Copyright The Linux Foundation and each contributor to LFX. +# SPDX-License-Identifier: MIT +builds: + - id: server + dir: ./cmd/server + ldflags: + - -X=main.Version={{.Env.VERSION}} + - -X=main.BuildTime={{.Env.BUILD_TIME}} + - -X=main.GitCommit={{.Env.GIT_COMMIT}} diff --git a/charts/lfx-v2-auth-service/Chart.yaml b/charts/lfx-v2-auth-service/Chart.yaml index 5e98281..313d91f 100644 --- a/charts/lfx-v2-auth-service/Chart.yaml +++ b/charts/lfx-v2-auth-service/Chart.yaml @@ -5,5 +5,5 @@ apiVersion: v2 name: lfx-v2-auth-service description: LFX Platform V2 Auth Service chart type: application -version: 0.3.4 +version: 0.3.5 appVersion: "latest" diff --git a/charts/lfx-v2-auth-service/templates/deployment.yaml b/charts/lfx-v2-auth-service/templates/deployment.yaml index 325361b..0726cfa 100644 --- a/charts/lfx-v2-auth-service/templates/deployment.yaml +++ b/charts/lfx-v2-auth-service/templates/deployment.yaml @@ -35,6 +35,59 @@ spec: {{- toYaml $config.valueFrom | nindent 14 }} {{- end }} {{- end }} + {{- with .Values.app.extraEnv }} + {{- toYaml . | nindent 10 }} + {{- end }} + {{- $otelServiceName := .Values.app.otel.serviceName | toString | trim }} + {{- if ne $otelServiceName "" }} + - name: OTEL_SERVICE_NAME + value: {{ $otelServiceName | quote }} + {{- end }} + {{- $otelServiceVersion := .Values.app.otel.serviceVersion | toString | trim }} + {{- if ne $otelServiceVersion "" }} + - name: OTEL_SERVICE_VERSION + value: {{ $otelServiceVersion | quote }} + {{- end }} + {{- $otelEndpoint := .Values.app.otel.endpoint | toString | trim }} + {{- if ne $otelEndpoint "" }} + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ $otelEndpoint | quote }} + {{- end }} + {{- $otelProtocol := .Values.app.otel.protocol | toString | trim }} + {{- if ne $otelProtocol "" }} + - name: OTEL_EXPORTER_OTLP_PROTOCOL + value: {{ $otelProtocol | quote }} + {{- end }} + {{- $otelInsecure := .Values.app.otel.insecure | toString | trim }} + {{- if ne $otelInsecure "" }} + - name: OTEL_EXPORTER_OTLP_INSECURE + value: {{ $otelInsecure | quote }} + {{- end }} + {{- $otelTracesExporter := .Values.app.otel.tracesExporter | toString | trim }} + {{- if ne $otelTracesExporter "" }} + - name: OTEL_TRACES_EXPORTER + value: {{ $otelTracesExporter | quote }} + {{- end }} + {{- $otelTracesSampleRatio := .Values.app.otel.tracesSampleRatio | toString | trim }} + {{- if ne $otelTracesSampleRatio "" }} + - name: OTEL_TRACES_SAMPLE_RATIO + value: {{ $otelTracesSampleRatio | quote }} + {{- end }} + {{- $otelMetricsExporter := .Values.app.otel.metricsExporter | toString | trim }} + {{- if ne $otelMetricsExporter "" }} + - name: OTEL_METRICS_EXPORTER + value: {{ $otelMetricsExporter | quote }} + {{- end }} + {{- $otelLogsExporter := .Values.app.otel.logsExporter | toString | trim }} + {{- if ne $otelLogsExporter "" }} + - name: OTEL_LOGS_EXPORTER + value: {{ $otelLogsExporter | quote }} + {{- end }} + {{- $otelPropagators := .Values.app.otel.propagators | toString | trim }} + {{- if ne $otelPropagators "" }} + - name: OTEL_PROPAGATORS + value: {{ $otelPropagators | quote }} + {{- end }} ports: - containerPort: {{ .Values.service.port }} name: web diff --git a/charts/lfx-v2-auth-service/values.yaml b/charts/lfx-v2-auth-service/values.yaml index 9f66b74..46cc398 100644 --- a/charts/lfx-v2-auth-service/values.yaml +++ b/charts/lfx-v2-auth-service/values.yaml @@ -138,3 +138,41 @@ app: value: authelia-users AUTHELIA_OIDC_USERINFO_URL: value: https://auth.k8s.orb.local/api/oidc/userinfo + # extraEnv allows injecting additional environment variables before + # other configurations + extraEnv: [] + # otel is the configuration for OpenTelemetry tracing + otel: + # serviceName is the service name for OpenTelemetry resource identification + # (default: "lfx-v2-auth-service") + serviceName: "" + # serviceVersion is the service version for OpenTelemetry resource + # identification + # (default: build-time version from ldflags) + 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" + # tracesSampleRatio specifies the sampling ratio for traces (0.0 to 1.0) + # A value of 1.0 means all traces are sampled, 0.5 means 50% are sampled + # (default: "1.0") + tracesSampleRatio: "1.0" + # metricsExporter specifies the metrics exporter: "otlp" or "none" + # (default: "none") + metricsExporter: "none" + # logsExporter specifies the logs exporter: "otlp" or "none" + # (default: "none") + logsExporter: "none" + # propagators specifies the propagators to use, comma-separated + # Supported values: "tracecontext", "baggage", "jaeger" + # (default: "tracecontext,baggage") + propagators: "tracecontext,baggage,jaeger" diff --git a/cmd/server/http.go b/cmd/server/http.go index 28b6ae0..dade313 100644 --- a/cmd/server/http.go +++ b/cmd/server/http.go @@ -13,6 +13,7 @@ import ( authservice "github.com/linuxfoundation/lfx-v2-auth-service/gen/auth_service" authserver "github.com/linuxfoundation/lfx-v2-auth-service/gen/http/auth_service/server" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "goa.design/clue/debug" goahttp "goa.design/goa/v3/http" ) @@ -60,6 +61,8 @@ func handleHTTPServer(ctx context.Context, host string, authEndpoints *authservi // Log query and response bodies if debug logs are enabled. handler = debug.HTTP()(handler) } + // Wrap the handler with OpenTelemetry instrumentation + handler = otelhttp.NewHandler(handler, "auth-service") // Start HTTP server using default configuration, change the code to // configure the server as required by your service. diff --git a/cmd/server/main.go b/cmd/server/main.go index 9ea818e..e445ad9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -18,6 +18,14 @@ import ( authservice "github.com/linuxfoundation/lfx-v2-auth-service/gen/auth_service" logging "github.com/linuxfoundation/lfx-v2-auth-service/pkg/log" + "github.com/linuxfoundation/lfx-v2-auth-service/pkg/utils" +) + +// Build-time variables set via ldflags +var ( + Version = "dev" + BuildTime = "unknown" + GitCommit = "unknown" ) const ( @@ -47,6 +55,28 @@ func main() { flag.Parse() ctx := context.Background() + + // Set up OpenTelemetry SDK. + // Command-line/environment OTEL_SERVICE_VERSION takes precedence over + // the build-time Version variable. + otelConfig := utils.OTelConfigFromEnv() + if otelConfig.ServiceVersion == "" { + otelConfig.ServiceVersion = Version + } + otelShutdown, err := utils.SetupOTelSDKWithConfig(ctx, otelConfig) + if err != nil { + slog.ErrorContext(ctx, "error setting up OpenTelemetry SDK", "error", err) + os.Exit(1) + } + // Handle shutdown properly so nothing leaks. + defer func() { + shutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulShutdownSeconds*time.Second) + defer cancel() + if shutdownErr := otelShutdown(shutdownCtx); shutdownErr != nil { + slog.ErrorContext(ctx, "error shutting down OpenTelemetry SDK", "error", shutdownErr) + } + }() + slog.InfoContext(ctx, "Starting auth service", "bind", *bind, "http-port", *port, diff --git a/go.mod b/go.mod index c95212d..6532f51 100644 --- a/go.mod +++ b/go.mod @@ -11,13 +11,27 @@ require ( github.com/google/uuid v1.6.0 github.com/lestrrat-go/jwx/v2 v2.1.6 github.com/nats-io/nats.go v1.45.0 + github.com/remychantenay/slog-otel v1.3.4 github.com/stretchr/testify v1.11.1 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 + go.opentelemetry.io/contrib/propagators/jaeger v1.39.0 + go.opentelemetry.io/otel v1.40.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 + go.opentelemetry.io/otel/log v0.16.0 + go.opentelemetry.io/otel/sdk v1.40.0 + go.opentelemetry.io/otel/sdk/log v0.16.0 + go.opentelemetry.io/otel/sdk/metric v1.40.0 go.yaml.in/yaml/v2 v2.4.2 goa.design/clue v1.2.3 goa.design/goa/v3 v3.23.3 - golang.org/x/crypto v0.45.0 - golang.org/x/oauth2 v0.32.0 - golang.org/x/sync v0.18.0 + golang.org/x/crypto v0.47.0 + golang.org/x/oauth2 v0.34.0 + golang.org/x/sync v0.19.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.34.1 k8s.io/client-go v0.34.1 @@ -26,13 +40,17 @@ require ( require ( github.com/PuerkitoBio/rehttp v1.4.0 // indirect github.com/aws/smithy-go v1.23.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-chi/chi/v5 v5.2.3 // indirect github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect @@ -41,6 +59,7 @@ require ( github.com/gohugoio/hashstructure v0.6.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect @@ -57,24 +76,29 @@ require ( github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/x448/float16 v0.8.4 // indirect go.devnw.com/structs v1.0.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.39.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect - google.golang.org/grpc v1.77.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.40.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/api v0.34.1 // indirect diff --git a/go.sum b/go.sum index 97aa58d..895ceff 100644 --- a/go.sum +++ b/go.sum @@ -8,12 +8,15 @@ github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5r github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0/go.mod h1:6L7zgvqo0idzI7IO8de6ZC051AfXb5ipkIJ7bIA2tGA= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/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= -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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZsmLR/+RGffQSXwEkXgfLSA08qDn9AI= @@ -21,10 +24,13 @@ github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598/go.mod h1:0FpDmbr github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/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= @@ -60,6 +66,8 @@ 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.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -115,10 +123,13 @@ github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -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/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/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/remychantenay/slog-otel v1.3.4 h1:xoM41ayLff2U8zlK5PH31XwD7Lk3W9wKfl4+RcmKom4= +github.com/remychantenay/slog-otel v1.3.4/go.mod h1:ZkazuFMICKGDrO0r1njxKRdjTt/YcXKn6v2+0q/b0+U= +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/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -144,18 +155,46 @@ go.devnw.com/structs v1.0.0 h1:FFkBoBOkapCdxFEIkpOZRmMOMr9b9hxjKTD3bJYl9lk= go.devnw.com/structs v1.0.0/go.mod h1:wHBkdQpNeazdQHszJ2sxwVEpd8zGTEsKkeywDLGbrmg= 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/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/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/contrib/propagators/jaeger v1.39.0 h1:Gz3yKzfMSEFzF0Vy5eIpu9ndpo4DhXMCxsLMF0OOApo= +go.opentelemetry.io/contrib/propagators/jaeger v1.39.0/go.mod h1:2D/cxxCqTlrday0rZrPujjg5aoAdqk1NaNyoXn8FJn8= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= +go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4= +go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI= +go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4= +go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -167,61 +206,63 @@ goa.design/goa/v3 v3.23.3/go.mod h1:DaJ9yv5WoXrpolbzouDj0A0o5Os0rPTTHy4aSebYVuI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= diff --git a/internal/infrastructure/authelia/user.go b/internal/infrastructure/authelia/user.go index de40b36..2e67417 100644 --- a/internal/infrastructure/authelia/user.go +++ b/internal/infrastructure/authelia/user.go @@ -479,7 +479,7 @@ func NewUserReaderWriter(ctx context.Context, config map[string]string, natsClie errSyncUsers := u.sync.syncUsers(ctx, u.storage, u.orchestrator) if errSyncUsers != nil { - slog.Warn("failed to sync from storage to orchestrator", "error", errSyncUsers) + slog.WarnContext(ctx, "failed to sync from storage to orchestrator", "error", errSyncUsers) } return u, nil diff --git a/pkg/httpclient/client.go b/pkg/httpclient/client.go index 1b878dc..12bc2aa 100644 --- a/pkg/httpclient/client.go +++ b/pkg/httpclient/client.go @@ -11,6 +11,8 @@ import ( "net/http" "strings" "time" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) // Client represents a generic HTTP client with retry logic @@ -155,12 +157,14 @@ func (c *Client) Request(ctx context.Context, verb, url string, body io.Reader, return c.Do(ctx, req) } -// NewClient creates a new HTTP client with the given configuration +// NewClient creates a new HTTP client with the given configuration. +// The client is instrumented with OpenTelemetry for distributed tracing. func NewClient(config Config) *Client { return &Client{ config: config, httpClient: &http.Client{ - Timeout: config.Timeout, + Timeout: config.Timeout, + Transport: otelhttp.NewTransport(http.DefaultTransport), }, } } diff --git a/pkg/log/log.go b/pkg/log/log.go index 6014d1d..d15db4c 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -8,6 +8,8 @@ import ( "log" "log/slog" "os" + + slogotel "github.com/remychantenay/slog-otel" ) type ctxKey string @@ -101,7 +103,12 @@ func InitStructureLogConfig() { } h = slog.NewJSONHandler(os.Stdout, logOptions) log.SetFlags(log.Llongfile) - logger := contextHandler{h} + + // Wrap with slog-otel handler to add trace_id and span_id from context + otelHandler := slogotel.OtelHandler{Next: h} + + // Wrap with contextHandler to support context-based attributes + logger := contextHandler{otelHandler} slog.SetDefault(slog.New(logger)) } diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go new file mode 100644 index 0000000..fb816ff --- /dev/null +++ b/pkg/log/log_test.go @@ -0,0 +1,345 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package log + +import ( + "bytes" + "context" + "log/slog" + "testing" + "time" +) + +func TestAppendCtx(t *testing.T) { + tests := []struct { + name string + parentCtx context.Context + attr slog.Attr + existingAttrs []slog.Attr + expectedLen int + }{ + { + name: "nil parent context", + parentCtx: nil, + attr: slog.String("key", "value"), + expectedLen: 1, + }, + { + name: "empty context", + parentCtx: context.Background(), + attr: slog.String("key", "value"), + expectedLen: 1, + }, + { + name: "context with existing attrs", + parentCtx: context.WithValue(context.Background(), slogFields, []slog.Attr{slog.String("existing", "attr")}), + attr: slog.String("new", "attr"), + existingAttrs: []slog.Attr{slog.String("existing", "attr")}, + expectedLen: 2, + }, + { + name: "integer attribute", + parentCtx: context.Background(), + attr: slog.Int("count", 42), + expectedLen: 1, + }, + { + name: "bool attribute", + parentCtx: context.Background(), + attr: slog.Bool("enabled", true), + expectedLen: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := AppendCtx(tt.parentCtx, tt.attr) + + if result == nil { + t.Error("AppendCtx() returned nil context") + return + } + + attrs, ok := result.Value(slogFields).([]slog.Attr) + if !ok { + t.Error("AppendCtx() did not store attrs in context") + return + } + + if len(attrs) != tt.expectedLen { + t.Errorf("AppendCtx() stored %d attrs, want %d", len(attrs), tt.expectedLen) + } + + // Verify the new attr is in the slice + found := false + for _, a := range attrs { + if a.Key == tt.attr.Key && a.Value.String() == tt.attr.Value.String() { + found = true + break + } + } + if !found { + t.Errorf("AppendCtx() did not include the new attribute %v", tt.attr) + } + }) + } +} + +func TestAppendCtxMultipleAttrs(t *testing.T) { + t.Run("chaining multiple AppendCtx calls", func(t *testing.T) { + ctx := context.Background() + ctx = AppendCtx(ctx, slog.String("first", "1")) + ctx = AppendCtx(ctx, slog.String("second", "2")) + ctx = AppendCtx(ctx, slog.String("third", "3")) + + attrs, ok := ctx.Value(slogFields).([]slog.Attr) + if !ok { + t.Fatal("could not retrieve attrs from context") + } + + if len(attrs) != 3 { + t.Errorf("expected 3 attrs, got %d", len(attrs)) + } + + expectedKeys := []string{"first", "second", "third"} + for i, key := range expectedKeys { + if attrs[i].Key != key { + t.Errorf("attr[%d].Key = %q, want %q", i, attrs[i].Key, key) + } + } + }) +} + +func TestPriority(t *testing.T) { + tests := []struct { + name string + level string + expected string + }{ + { + name: "critical level", + level: "critical", + expected: "critical", + }, + { + name: "warning level", + level: "warning", + expected: "warning", + }, + { + name: "info level", + level: "info", + expected: "info", + }, + { + name: "empty level", + level: "", + expected: "", + }, + { + name: "custom level", + level: "custom-priority", + expected: "custom-priority", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Priority(tt.level) + + if result.Key != "priority" { + t.Errorf("Priority() key = %q, want %q", result.Key, "priority") + } + + if result.Value.String() != tt.expected { + t.Errorf("Priority(%q) value = %q, want %q", tt.level, result.Value.String(), tt.expected) + } + }) + } +} + +func TestPriorityCritical(t *testing.T) { + t.Run("returns critical priority attr", func(t *testing.T) { + result := PriorityCritical() + + if result.Key != "priority" { + t.Errorf("PriorityCritical() key = %q, want %q", result.Key, "priority") + } + + if result.Value.String() != priorityCritical { + t.Errorf("PriorityCritical() value = %q, want %q", result.Value.String(), priorityCritical) + } + }) +} + +// mockHandler is a test handler that captures records +type mockHandler struct { + records []slog.Record + attrs []slog.Attr +} + +func (m *mockHandler) Enabled(_ context.Context, _ slog.Level) bool { + return true +} + +func (m *mockHandler) Handle(_ context.Context, r slog.Record) error { + m.records = append(m.records, r) + r.Attrs(func(a slog.Attr) bool { + m.attrs = append(m.attrs, a) + return true + }) + return nil +} + +func (m *mockHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return m +} + +func (m *mockHandler) WithGroup(name string) slog.Handler { + return m +} + +func TestContextHandlerHandle(t *testing.T) { + tests := []struct { + name string + ctxAttrs []slog.Attr + recordMsg string + expectedAttrs int + }{ + { + name: "no context attrs", + ctxAttrs: nil, + recordMsg: "test message", + expectedAttrs: 0, + }, + { + name: "single context attr", + ctxAttrs: []slog.Attr{slog.String("key", "value")}, + recordMsg: "test message", + expectedAttrs: 1, + }, + { + name: "multiple context attrs", + ctxAttrs: []slog.Attr{ + slog.String("key1", "value1"), + slog.String("key2", "value2"), + slog.Int("key3", 123), + }, + recordMsg: "test message", + expectedAttrs: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mock := &mockHandler{} + handler := contextHandler{Handler: mock} + + ctx := context.Background() + if tt.ctxAttrs != nil { + ctx = context.WithValue(ctx, slogFields, tt.ctxAttrs) + } + + record := slog.NewRecord(time.Now(), slog.LevelInfo, tt.recordMsg, 0) + err := handler.Handle(ctx, record) + + if err != nil { + t.Errorf("Handle() returned error: %v", err) + } + + if len(mock.attrs) != tt.expectedAttrs { + t.Errorf("Handle() captured %d attrs, want %d", len(mock.attrs), tt.expectedAttrs) + } + }) + } +} + +func TestContextHandlerHandleWithLogger(t *testing.T) { + t.Run("context attrs appear in log output", func(t *testing.T) { + var buf bytes.Buffer + baseHandler := slog.NewJSONHandler(&buf, nil) + handler := contextHandler{Handler: baseHandler} + logger := slog.New(handler) + + ctx := context.Background() + ctx = AppendCtx(ctx, slog.String("request_id", "test-123")) + ctx = AppendCtx(ctx, slog.String("user_id", "user-456")) + + logger.InfoContext(ctx, "test log message") + + output := buf.String() + if output == "" { + t.Error("no log output captured") + return + } + + // Verify context attrs are in the output + if !bytes.Contains(buf.Bytes(), []byte(`"request_id":"test-123"`)) { + t.Errorf("log output missing request_id: %s", output) + } + if !bytes.Contains(buf.Bytes(), []byte(`"user_id":"user-456"`)) { + t.Errorf("log output missing user_id: %s", output) + } + }) +} + +func TestInitStructureLogConfig(t *testing.T) { + tests := []struct { + name string + logLevel string + addSource string + wantLogLevel slog.Level + }{ + { + name: "debug level", + logLevel: "debug", + addSource: "false", + wantLogLevel: slog.LevelDebug, + }, + { + name: "info level", + logLevel: "info", + addSource: "false", + wantLogLevel: slog.LevelInfo, + }, + { + name: "warn level", + logLevel: "warn", + addSource: "false", + wantLogLevel: slog.LevelWarn, + }, + { + name: "default level on invalid", + logLevel: "invalid", + addSource: "false", + wantLogLevel: slog.LevelDebug, + }, + { + name: "default level on empty", + logLevel: "", + addSource: "false", + wantLogLevel: slog.LevelDebug, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prev := slog.Default() + defer slog.SetDefault(prev) + + // Set test env vars + t.Setenv("LOG_LEVEL", tt.logLevel) + t.Setenv("LOG_ADD_SOURCE", tt.addSource) + + // Call the function - it modifies global state + InitStructureLogConfig() + + // Verify the default logger is enabled at expected level + logger := slog.Default() + if !logger.Enabled(context.Background(), tt.wantLogLevel) { + t.Errorf("logger not enabled at level %v", tt.wantLogLevel) + } + }) + } +} diff --git a/pkg/utils/otel.go b/pkg/utils/otel.go new file mode 100644 index 0000000..4a08b9c --- /dev/null +++ b/pkg/utils/otel.go @@ -0,0 +1,395 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "context" + "errors" + "log/slog" + "os" + "strconv" + "strings" + "time" + + "go.opentelemetry.io/contrib/propagators/jaeger" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/log/global" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/log" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.39.0" +) + +const ( + // OTelProtocolGRPC configures OTLP exporters to use gRPC protocol. + OTelProtocolGRPC = "grpc" + // OTelProtocolHTTP configures OTLP exporters to use HTTP protocol. + OTelProtocolHTTP = "http" + + // OTelExporterOTLP configures signals to export via OTLP. + OTelExporterOTLP = "otlp" + // OTelExporterNone disables exporting for a signal. + OTelExporterNone = "none" +) + +// OTelConfig holds OpenTelemetry configuration options. +type OTelConfig struct { + // ServiceName is the name of the service for resource identification. + // Env: OTEL_SERVICE_NAME (default: "lfx-v2-auth-service") + ServiceName string + // ServiceVersion is the version of the service. + // Env: OTEL_SERVICE_VERSION + ServiceVersion string + // Protocol specifies the OTLP protocol to use: "grpc" or "http". + // Env: OTEL_EXPORTER_OTLP_PROTOCOL (default: "grpc") + Protocol string + // Endpoint is the OTLP collector endpoint. + // For gRPC: typically "localhost:4317" + // For HTTP: typically "localhost:4318" + // Env: OTEL_EXPORTER_OTLP_ENDPOINT + Endpoint string + // Insecure disables TLS for the connection. + // Env: OTEL_EXPORTER_OTLP_INSECURE (set to "true" for insecure connections) + Insecure bool + // TracesExporter specifies the traces exporter: "otlp" or "none". + // Env: OTEL_TRACES_EXPORTER (default: "none") + TracesExporter string + // TracesSampleRatio specifies the sampling ratio for traces (0.0 to 1.0). + // A value of 1.0 means all traces are sampled, 0.5 means 50% are sampled. + // Env: OTEL_TRACES_SAMPLE_RATIO (default: 1.0) + TracesSampleRatio float64 + // MetricsExporter specifies the metrics exporter: "otlp" or "none". + // Env: OTEL_METRICS_EXPORTER (default: "none") + MetricsExporter string + // LogsExporter specifies the logs exporter: "otlp" or "none". + // Env: OTEL_LOGS_EXPORTER (default: "none") + LogsExporter string + // Propagators specifies the propagators to use, comma-separated. + // Supported values: "tracecontext", "baggage", "jaeger" + // Env: OTEL_PROPAGATORS (default: "tracecontext,baggage,jaeger") + Propagators string +} + +// OTelConfigFromEnv creates an OTelConfig from environment variables. +// See OTelConfig struct fields for supported environment variables. +func OTelConfigFromEnv() OTelConfig { + serviceName := os.Getenv("OTEL_SERVICE_NAME") + if serviceName == "" { + serviceName = "lfx-v2-auth-service" + } + + serviceVersion := os.Getenv("OTEL_SERVICE_VERSION") + + protocol := os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") + if protocol == "" { + protocol = OTelProtocolGRPC + } + + endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") + + insecure := os.Getenv("OTEL_EXPORTER_OTLP_INSECURE") == "true" + + tracesExporter := os.Getenv("OTEL_TRACES_EXPORTER") + if tracesExporter == "" { + tracesExporter = OTelExporterNone + } + + metricsExporter := os.Getenv("OTEL_METRICS_EXPORTER") + if metricsExporter == "" { + metricsExporter = OTelExporterNone + } + + logsExporter := os.Getenv("OTEL_LOGS_EXPORTER") + if logsExporter == "" { + logsExporter = OTelExporterNone + } + + tracesSampleRatio := 1.0 + if ratio := os.Getenv("OTEL_TRACES_SAMPLE_RATIO"); ratio != "" { + if parsed, err := strconv.ParseFloat(ratio, 64); err == nil { + if parsed >= 0.0 && parsed <= 1.0 { + tracesSampleRatio = parsed + } else { + slog.Warn("OTEL_TRACES_SAMPLE_RATIO must be between 0.0 and 1.0, using default 1.0", + "provided-value", ratio) + } + } else { + slog.Warn("invalid OTEL_TRACES_SAMPLE_RATIO value, using default 1.0", + "provided-value", ratio, "error", err) + } + } + + propagators := os.Getenv("OTEL_PROPAGATORS") + if propagators == "" { + propagators = "tracecontext,baggage,jaeger" + } + + slog.With( + "service-name", serviceName, + "version", serviceVersion, + "protocol", protocol, + "endpoint", endpoint, + "insecure", insecure, + "traces-exporter", tracesExporter, + "traces-sample-ratio", tracesSampleRatio, + "metrics-exporter", metricsExporter, + "logs-exporter", logsExporter, + "propagators", propagators, + ).Debug("OTelConfig") + + return OTelConfig{ + ServiceName: serviceName, + ServiceVersion: serviceVersion, + Protocol: protocol, + Endpoint: endpoint, + Insecure: insecure, + TracesExporter: tracesExporter, + TracesSampleRatio: tracesSampleRatio, + MetricsExporter: metricsExporter, + LogsExporter: logsExporter, + Propagators: propagators, + } +} + +// SetupOTelSDK bootstraps the OpenTelemetry pipeline with OTLP exporters. +// 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 the provided configuration. +// If it does not return an error, make sure to call shutdown for proper cleanup. +func SetupOTelSDKWithConfig(ctx context.Context, cfg 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)) + } + + // Create resource with service information. + res, err := newResource(cfg) + if err != nil { + handleErr(err) + return + } + + // Set up propagator. + prop := newPropagator(cfg) + otel.SetTextMapPropagator(prop) + + // Set up trace provider if enabled. + if cfg.TracesExporter != OTelExporterNone { + var tracerProvider *trace.TracerProvider + tracerProvider, err = newTraceProvider(ctx, cfg, res) + if err != nil { + handleErr(err) + return + } + shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown) + otel.SetTracerProvider(tracerProvider) + } + + // Set up metrics provider if enabled. + if cfg.MetricsExporter != OTelExporterNone { + var metricsProvider *metric.MeterProvider + metricsProvider, err = newMetricsProvider(ctx, cfg, res) + if err != nil { + handleErr(err) + return + } + shutdownFuncs = append(shutdownFuncs, metricsProvider.Shutdown) + otel.SetMeterProvider(metricsProvider) + } + + // Set up logger provider if enabled. + if cfg.LogsExporter != OTelExporterNone { + var loggerProvider *log.LoggerProvider + loggerProvider, err = newLoggerProvider(ctx, cfg, res) + if err != nil { + handleErr(err) + return + } + shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown) + global.SetLoggerProvider(loggerProvider) + } + + return +} + +// newResource creates an OpenTelemetry resource with service name and version attributes. +func newResource(cfg OTelConfig) (*resource.Resource, error) { + return resource.Merge( + resource.Default(), + resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(cfg.ServiceName), + semconv.ServiceVersion(cfg.ServiceVersion), + ), + ) +} + +// newPropagator creates a composite text map propagator based on the configured propagators. +// Supported propagators: "tracecontext", "baggage", "jaeger" +func newPropagator(cfg OTelConfig) propagation.TextMapPropagator { + var propagators []propagation.TextMapPropagator + + for _, p := range strings.Split(cfg.Propagators, ",") { + p = strings.TrimSpace(p) + switch p { + case "tracecontext": + propagators = append(propagators, propagation.TraceContext{}) + case "baggage": + propagators = append(propagators, propagation.Baggage{}) + case "jaeger": + propagators = append(propagators, jaeger.Jaeger{}) + default: + slog.Warn("unknown propagator, skipping", "propagator", p) + } + } + + if len(propagators) == 0 { + // Fallback to defaults if no valid propagators configured + propagators = []propagation.TextMapPropagator{ + propagation.TraceContext{}, + propagation.Baggage{}, + jaeger.Jaeger{}, + } + } + + return propagation.NewCompositeTextMapPropagator(propagators...) +} + +// newTraceProvider creates a TracerProvider with an OTLP exporter configured based on the protocol setting. +func newTraceProvider(ctx context.Context, cfg OTelConfig, res *resource.Resource) (*trace.TracerProvider, error) { + var exporter trace.SpanExporter + var err error + + if cfg.Protocol == OTelProtocolHTTP { + var opts []otlptracehttp.Option + if cfg.Endpoint != "" { + opts = append(opts, otlptracehttp.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + opts = append(opts, otlptracehttp.WithInsecure()) + } + exporter, err = otlptracehttp.New(ctx, opts...) + } else { + var opts []otlptracegrpc.Option + if cfg.Endpoint != "" { + opts = append(opts, otlptracegrpc.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + opts = append(opts, otlptracegrpc.WithInsecure()) + } + exporter, err = otlptracegrpc.New(ctx, opts...) + } + + if err != nil { + return nil, err + } + + traceProvider := trace.NewTracerProvider( + trace.WithResource(res), + trace.WithSampler(trace.TraceIDRatioBased(cfg.TracesSampleRatio)), + trace.WithBatcher(exporter, + trace.WithBatchTimeout(time.Second), + ), + ) + return traceProvider, nil +} + +// newMetricsProvider creates a MeterProvider with an OTLP exporter configured based on the protocol setting. +func newMetricsProvider(ctx context.Context, cfg OTelConfig, res *resource.Resource) (*metric.MeterProvider, error) { + var exporter metric.Exporter + var err error + + if cfg.Protocol == OTelProtocolHTTP { + var opts []otlpmetrichttp.Option + if cfg.Endpoint != "" { + opts = append(opts, otlpmetrichttp.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + opts = append(opts, otlpmetrichttp.WithInsecure()) + } + exporter, err = otlpmetrichttp.New(ctx, opts...) + } else { + var opts []otlpmetricgrpc.Option + if cfg.Endpoint != "" { + opts = append(opts, otlpmetricgrpc.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + opts = append(opts, otlpmetricgrpc.WithInsecure()) + } + exporter, err = otlpmetricgrpc.New(ctx, opts...) + } + + if err != nil { + return nil, err + } + + metricsProvider := metric.NewMeterProvider( + metric.WithResource(res), + metric.WithReader(metric.NewPeriodicReader(exporter, + metric.WithInterval(30*time.Second), + )), + ) + return metricsProvider, nil +} + +// newLoggerProvider creates a LoggerProvider with an OTLP exporter configured based on the protocol setting. +func newLoggerProvider(ctx context.Context, cfg OTelConfig, res *resource.Resource) (*log.LoggerProvider, error) { + var exporter log.Exporter + var err error + + if cfg.Protocol == OTelProtocolHTTP { + var opts []otlploghttp.Option + if cfg.Endpoint != "" { + opts = append(opts, otlploghttp.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + opts = append(opts, otlploghttp.WithInsecure()) + } + exporter, err = otlploghttp.New(ctx, opts...) + } else { + var opts []otlploggrpc.Option + if cfg.Endpoint != "" { + opts = append(opts, otlploggrpc.WithEndpoint(cfg.Endpoint)) + } + if cfg.Insecure { + opts = append(opts, otlploggrpc.WithInsecure()) + } + exporter, err = otlploggrpc.New(ctx, opts...) + } + + if err != nil { + return nil, err + } + + loggerProvider := log.NewLoggerProvider( + log.WithResource(res), + log.WithProcessor(log.NewBatchProcessor(exporter)), + ) + return loggerProvider, nil +} diff --git a/pkg/utils/otel_test.go b/pkg/utils/otel_test.go new file mode 100644 index 0000000..a76f269 --- /dev/null +++ b/pkg/utils/otel_test.go @@ -0,0 +1,425 @@ +// Copyright The Linux Foundation and each contributor to LFX. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "context" + "testing" +) + +// TestOTelConfigFromEnv_Defaults verifies that OTelConfigFromEnv returns +// sensible default values when no environment variables are set. +func TestOTelConfigFromEnv_Defaults(t *testing.T) { + cfg := OTelConfigFromEnv() + + if cfg.ServiceName != "lfx-v2-auth-service" { + t.Errorf("expected default ServiceName 'lfx-v2-auth-service', got %q", cfg.ServiceName) + } + if cfg.ServiceVersion != "" { + t.Errorf("expected empty ServiceVersion, got %q", cfg.ServiceVersion) + } + if cfg.Protocol != OTelProtocolGRPC { + t.Errorf("expected default Protocol %q, got %q", OTelProtocolGRPC, cfg.Protocol) + } + if cfg.Endpoint != "" { + t.Errorf("expected empty Endpoint, got %q", cfg.Endpoint) + } + if cfg.Insecure != false { + t.Errorf("expected Insecure false, got %t", cfg.Insecure) + } + if cfg.TracesExporter != OTelExporterNone { + t.Errorf("expected default TracesExporter %q, got %q", OTelExporterNone, cfg.TracesExporter) + } + if cfg.TracesSampleRatio != 1.0 { + t.Errorf("expected default TracesSampleRatio 1.0, got %f", cfg.TracesSampleRatio) + } + if cfg.MetricsExporter != OTelExporterNone { + t.Errorf("expected default MetricsExporter %q, got %q", OTelExporterNone, cfg.MetricsExporter) + } + if cfg.LogsExporter != OTelExporterNone { + t.Errorf("expected default LogsExporter %q, got %q", OTelExporterNone, cfg.LogsExporter) + } +} + +// TestOTelConfigFromEnv_CustomValues verifies that OTelConfigFromEnv correctly +// reads and parses all supported OTEL_* environment variables. +func TestOTelConfigFromEnv_CustomValues(t *testing.T) { + t.Setenv("OTEL_SERVICE_NAME", "test-service") + t.Setenv("OTEL_SERVICE_VERSION", "1.2.3") + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "http") + t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4318") + t.Setenv("OTEL_EXPORTER_OTLP_INSECURE", "true") + t.Setenv("OTEL_TRACES_EXPORTER", "otlp") + t.Setenv("OTEL_TRACES_SAMPLE_RATIO", "0.5") + t.Setenv("OTEL_METRICS_EXPORTER", "otlp") + t.Setenv("OTEL_LOGS_EXPORTER", "otlp") + + cfg := OTelConfigFromEnv() + + if cfg.ServiceName != "test-service" { + t.Errorf("expected ServiceName 'test-service', got %q", cfg.ServiceName) + } + if cfg.ServiceVersion != "1.2.3" { + t.Errorf("expected ServiceVersion '1.2.3', got %q", cfg.ServiceVersion) + } + if cfg.Protocol != OTelProtocolHTTP { + t.Errorf("expected Protocol %q, got %q", OTelProtocolHTTP, cfg.Protocol) + } + if cfg.Endpoint != "localhost:4318" { + t.Errorf("expected Endpoint 'localhost:4318', got %q", cfg.Endpoint) + } + if cfg.Insecure != true { + t.Errorf("expected Insecure true, got %t", cfg.Insecure) + } + if cfg.TracesExporter != OTelExporterOTLP { + t.Errorf("expected TracesExporter %q, got %q", OTelExporterOTLP, cfg.TracesExporter) + } + if cfg.TracesSampleRatio != 0.5 { + t.Errorf("expected TracesSampleRatio 0.5, got %f", cfg.TracesSampleRatio) + } + if cfg.MetricsExporter != OTelExporterOTLP { + t.Errorf("expected MetricsExporter %q, got %q", OTelExporterOTLP, cfg.MetricsExporter) + } + if cfg.LogsExporter != OTelExporterOTLP { + t.Errorf("expected LogsExporter %q, got %q", OTelExporterOTLP, cfg.LogsExporter) + } +} + +// TestOTelConfigFromEnv_UnsupportedProtocol verifies that an unsupported protocol +// value is passed through as-is (defaults to gRPC behavior in the provider functions). +func TestOTelConfigFromEnv_UnsupportedProtocol(t *testing.T) { + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", "unsupported") + + cfg := OTelConfigFromEnv() + + if cfg.Protocol != "unsupported" { + t.Errorf("expected Protocol 'unsupported', got %q", cfg.Protocol) + } +} + +// TestOTelConfigFromEnv_TracesSampleRatio tests the parsing and validation of +// the OTEL_TRACES_SAMPLE_RATIO environment variable, including edge cases like +// invalid values, out-of-range numbers, and empty strings. +func TestOTelConfigFromEnv_TracesSampleRatio(t *testing.T) { + tests := []struct { + name string + envValue string + expectedRatio float64 + }{ + {"valid zero", "0.0", 0.0}, + {"valid half", "0.5", 0.5}, + {"valid one", "1.0", 1.0}, + {"valid small", "0.01", 0.01}, + {"invalid negative", "-0.5", 1.0}, // defaults to 1.0 + {"invalid above one", "1.5", 1.0}, // defaults to 1.0 + {"invalid non-number", "invalid", 1.0}, // defaults to 1.0 + {"empty string", "", 1.0}, // defaults to 1.0 + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envValue != "" { + t.Setenv("OTEL_TRACES_SAMPLE_RATIO", tt.envValue) + } + + cfg := OTelConfigFromEnv() + + if cfg.TracesSampleRatio != tt.expectedRatio { + t.Errorf("expected TracesSampleRatio %f, got %f", tt.expectedRatio, cfg.TracesSampleRatio) + } + }) + } +} + +// TestOTelConfigFromEnv_InsecureFlag tests the parsing of the +// OTEL_EXPORTER_OTLP_INSECURE environment variable. Only the literal string +// "true" enables insecure mode; all other values default to false. +func TestOTelConfigFromEnv_InsecureFlag(t *testing.T) { + tests := []struct { + name string + envValue string + expected bool + }{ + {"true", "true", true}, + {"false", "false", false}, + {"empty", "", false}, + {"TRUE uppercase", "TRUE", false}, // only "true" is recognized + {"1", "1", false}, // only "true" is recognized + {"yes", "yes", false}, // only "true" is recognized + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envValue != "" { + t.Setenv("OTEL_EXPORTER_OTLP_INSECURE", tt.envValue) + } + + cfg := OTelConfigFromEnv() + + if cfg.Insecure != tt.expected { + t.Errorf("expected Insecure %t, got %t", tt.expected, cfg.Insecure) + } + }) + } +} + +// TestSetupOTelSDKWithConfig_AllDisabled verifies that the SDK can be +// initialized successfully when all exporters (traces, metrics, logs) are +// disabled, and that the returned shutdown function works correctly. +func TestSetupOTelSDKWithConfig_AllDisabled(t *testing.T) { + cfg := OTelConfig{ + ServiceName: "test-service", + ServiceVersion: "1.0.0", + Protocol: OTelProtocolGRPC, + TracesExporter: OTelExporterNone, + TracesSampleRatio: 1.0, + MetricsExporter: OTelExporterNone, + LogsExporter: OTelExporterNone, + } + + ctx := context.Background() + shutdown, err := SetupOTelSDKWithConfig(ctx, cfg) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if shutdown == nil { + t.Fatal("expected non-nil shutdown function") + } + + // Call shutdown to ensure it works without error + err = shutdown(ctx) + if err != nil { + t.Errorf("shutdown returned unexpected error: %v", err) + } +} + +// TestSetupOTelSDKWithConfig_ShutdownIdempotent verifies that the shutdown +// function can be called multiple times without error. This is important for +// graceful shutdown scenarios where shutdown may be triggered multiple times. +func TestSetupOTelSDKWithConfig_ShutdownIdempotent(t *testing.T) { + cfg := OTelConfig{ + ServiceName: "test-service", + ServiceVersion: "1.0.0", + Protocol: OTelProtocolGRPC, + TracesExporter: OTelExporterNone, + TracesSampleRatio: 1.0, + MetricsExporter: OTelExporterNone, + LogsExporter: OTelExporterNone, + } + + ctx := context.Background() + shutdown, err := SetupOTelSDKWithConfig(ctx, cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Call shutdown multiple times + err = shutdown(ctx) + if err != nil { + t.Errorf("first shutdown returned unexpected error: %v", err) + } + + // Second call should also succeed (shutdownFuncs is cleared) + err = shutdown(ctx) + if err != nil { + t.Errorf("second shutdown returned unexpected error: %v", err) + } +} + +// TestNewResource verifies that newResource creates a valid OpenTelemetry +// resource with the expected service.name attribute for various input values, +// including edge cases like empty versions and unicode characters. +func TestNewResource(t *testing.T) { + tests := []struct { + name string + serviceName string + serviceVersion string + }{ + {"basic", "test-service", "1.0.0"}, + {"empty version", "test-service", ""}, + {"unicode name", "测试服务", "2.0.0"}, + {"special chars", "test-service-123", "1.0.0-beta.1"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := OTelConfig{ + ServiceName: tt.serviceName, + ServiceVersion: tt.serviceVersion, + } + + res, err := newResource(cfg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if res == nil { + t.Fatal("expected non-nil resource") + } + + // Verify resource contains expected attributes + attrs := res.Attributes() + found := false + for _, attr := range attrs { + if string(attr.Key) == "service.name" && attr.Value.AsString() == tt.serviceName { + found = true + break + } + } + if !found { + t.Errorf("resource missing service.name attribute with value %q", tt.serviceName) + } + }) + } +} + +// TestNewPropagator verifies that newPropagator returns a composite +// TextMapPropagator that includes the standard W3C trace context fields +// (traceparent, tracestate), baggage propagation, and jaeger propagation. +func TestNewPropagator(t *testing.T) { + cfg := OTelConfig{Propagators: "tracecontext,baggage,jaeger"} + prop := newPropagator(cfg) + + if prop == nil { + t.Fatal("expected non-nil propagator") + } + + // Verify it's a composite propagator with expected fields + fields := prop.Fields() + if len(fields) == 0 { + t.Error("expected propagator to have fields") + } + + // Check for expected propagation fields (traceparent, tracestate, baggage, uber-trace-id) + expectedFields := map[string]bool{ + "traceparent": false, + "tracestate": false, + "baggage": false, + "uber-trace-id": false, // jaeger propagator header + } + + for _, field := range fields { + expectedFields[field] = true + } + + for field, found := range expectedFields { + if !found { + t.Errorf("expected propagator to include field %q", field) + } + } +} + +// TestNewPropagator_Default verifies that the default propagators include +// tracecontext, baggage, and jaeger when no OTEL_PROPAGATORS is set. +func TestNewPropagator_Default(t *testing.T) { + cfg := OTelConfigFromEnv() + + if cfg.Propagators != "tracecontext,baggage,jaeger" { + t.Errorf("expected default Propagators to be 'tracecontext,baggage,jaeger', got %q", cfg.Propagators) + } + + prop := newPropagator(cfg) + fields := prop.Fields() + + // Should include uber-trace-id from jaeger propagator + hasJaeger := false + for _, field := range fields { + if field == "uber-trace-id" { + hasJaeger = true + break + } + } + if !hasJaeger { + t.Error("expected default propagator to include jaeger (uber-trace-id field)") + } +} + +// TestOTelConstants verifies that the exported OTel constants have their +// expected string values, ensuring API compatibility. +func TestOTelConstants(t *testing.T) { + // Verify constants have expected values + if OTelProtocolGRPC != "grpc" { + t.Errorf("expected OTelProtocolGRPC to be 'grpc', got %q", OTelProtocolGRPC) + } + if OTelProtocolHTTP != "http" { + t.Errorf("expected OTelProtocolHTTP to be 'http', got %q", OTelProtocolHTTP) + } + if OTelExporterOTLP != "otlp" { + t.Errorf("expected OTelExporterOTLP to be 'otlp', got %q", OTelExporterOTLP) + } + if OTelExporterNone != "none" { + t.Errorf("expected OTelExporterNone to be 'none', got %q", OTelExporterNone) + } +} + +// TestSetupOTelSDK tests the convenience function SetupOTelSDK which reads +// configuration from environment variables. With no env vars set, it should +// use defaults and successfully initialize the SDK. +func TestSetupOTelSDK(t *testing.T) { + ctx := context.Background() + shutdown, err := SetupOTelSDK(ctx) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if shutdown == nil { + t.Fatal("expected non-nil shutdown function") + } + + err = shutdown(ctx) + if err != nil { + t.Errorf("shutdown returned unexpected error: %v", err) + } +} + +// TestOTelConfig_ZeroValue documents that a zero-value OTelConfig does not +// have exporters disabled by default. Users must explicitly set exporters to +// OTelExporterNone to disable them. +func TestOTelConfig_ZeroValue(t *testing.T) { + // Test that zero-value config (with empty strings) tries to enable exporters + // because empty string != "none". This verifies the expected behavior that + // users should explicitly set exporters to "none" to disable them. + cfg := OTelConfig{} + + // Verify the zero-value behavior: empty string fields mean exporters would be enabled + if cfg.TracesExporter == OTelExporterNone { + t.Error("expected zero-value TracesExporter to NOT equal OTelExporterNone") + } + if cfg.MetricsExporter == OTelExporterNone { + t.Error("expected zero-value MetricsExporter to NOT equal OTelExporterNone") + } + if cfg.LogsExporter == OTelExporterNone { + t.Error("expected zero-value LogsExporter to NOT equal OTelExporterNone") + } +} + +// TestOTelConfig_MinimalConfig verifies that the SDK can be initialized with +// a minimal configuration where only the exporter settings are specified. +func TestOTelConfig_MinimalConfig(t *testing.T) { + // Test minimal config with all exporters explicitly disabled + cfg := OTelConfig{ + TracesExporter: OTelExporterNone, + MetricsExporter: OTelExporterNone, + LogsExporter: OTelExporterNone, + } + + ctx := context.Background() + shutdown, err := SetupOTelSDKWithConfig(ctx, cfg) + + if err != nil { + t.Fatalf("unexpected error with minimal config: %v", err) + } + + if shutdown == nil { + t.Fatal("expected non-nil shutdown function") + } + + err = shutdown(ctx) + if err != nil { + t.Errorf("shutdown returned unexpected error: %v", err) + } +}