diff --git a/charts/templates/configmap.yaml b/charts/templates/configmap.yaml index f5bf419..dcedb23 100644 --- a/charts/templates/configmap.yaml +++ b/charts/templates/configmap.yaml @@ -87,7 +87,6 @@ data: otel: enabled: {{ .Values.config.logging.otel.enabled }} - sampling_rate: {{ .Values.config.logging.otel.sampling_rate }} masking: enabled: {{ .Values.config.logging.masking.enabled }} diff --git a/charts/values.yaml b/charts/values.yaml index d2318f4..b89f59a 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -94,9 +94,11 @@ config: format: json output: stdout + # OpenTelemetry tracing (HyperFleet standard) + # Configuration via standard environment variables. + # See: https://github.com/openshift-hyperfleet/architecture/blob/main/hyperfleet/standards/tracing.md#configuration otel: - enabled: false - sampling_rate: 1.0 + enabled: true masking: enabled: true diff --git a/cmd/hyperfleet-api/servecmd/cmd.go b/cmd/hyperfleet-api/servecmd/cmd.go index 24364ae..b3d1a8c 100755 --- a/cmd/hyperfleet-api/servecmd/cmd.go +++ b/cmd/hyperfleet-api/servecmd/cmd.go @@ -5,6 +5,7 @@ import ( "log/slog" "os" "os/signal" + "strconv" "syscall" "github.com/spf13/cobra" @@ -69,14 +70,45 @@ func runServe(cmd *cobra.Command, args []string) { logger.Info(ctx, config.DumpConfig(environments.Environment().Config)) var tp *trace.TracerProvider - if environments.Environment().Config.Logging.OTel.Enabled { - samplingRate := environments.Environment().Config.Logging.OTel.SamplingRate - traceProvider, err := telemetry.InitTraceProvider(ctx, "hyperfleet-api", api.Version, samplingRate) + + // Check for deprecated HYPERFLEET_LOGGING_OTEL_ENABLED variable + if deprecatedEnv := os.Getenv("HYPERFLEET_LOGGING_OTEL_ENABLED"); deprecatedEnv != "" { + logger.With(ctx, + "deprecated_variable", "HYPERFLEET_LOGGING_OTEL_ENABLED", + "replacement", "TRACING_ENABLED", + ).Warn("HYPERFLEET_LOGGING_OTEL_ENABLED is deprecated and ignored. Please use TRACING_ENABLED instead.") + } + + // Determine if tracing is enabled using TRACING_ENABLED (tracing standard) + var tracingEnabled bool + if tracingEnv := os.Getenv("TRACING_ENABLED"); tracingEnv != "" { + if enabled, err := strconv.ParseBool(tracingEnv); err == nil { + tracingEnabled = enabled + } else { + logger.With(ctx, + logger.FieldTracingEnabled, tracingEnv, + "falling_back_to", environments.Environment().Config.Logging.OTel.Enabled). + WithError(err).Warn("Invalid TRACING_ENABLED value, falling back to config") + tracingEnabled = environments.Environment().Config.Logging.OTel.Enabled + } + } else { + // Use config default if TRACING_ENABLED not set + tracingEnabled = environments.Environment().Config.Logging.OTel.Enabled + } + + if tracingEnabled { + // OpenTelemetry configuration is driven entirely by standard environment variables: + serviceName := "hyperfleet-api" + if svcName := os.Getenv("OTEL_SERVICE_NAME"); svcName != "" { + serviceName = svcName + } + + traceProvider, err := telemetry.InitTraceProvider(ctx, serviceName, api.Version) if err != nil { logger.WithError(ctx, err).Warn("Failed to initialize OpenTelemetry") } else { tp = traceProvider - logger.With(ctx, logger.FieldSamplingRate, samplingRate).Info("OpenTelemetry initialized") + logger.With(ctx, logger.FieldServiceName, serviceName).Info("OpenTelemetry initialized") } } else { logger.With(ctx, logger.FieldOTelEnabled, false).Info("OpenTelemetry disabled") @@ -89,7 +121,7 @@ func runServe(cmd *cobra.Command, args []string) { "masking_enabled", environments.Environment().Config.Logging.Masking.Enabled, ).Info("Logger initialized") - apiServer := server.NewAPIServer() + apiServer := server.NewAPIServer(tracingEnabled) go apiServer.Start() metricsServer := server.NewMetricsServer() diff --git a/cmd/hyperfleet-api/server/api_server.go b/cmd/hyperfleet-api/server/api_server.go index a2fb99c..03f00d4 100755 --- a/cmd/hyperfleet-api/server/api_server.go +++ b/cmd/hyperfleet-api/server/api_server.go @@ -25,10 +25,10 @@ func env() *environments.Env { return environments.Environment() } -func NewAPIServer() Server { +func NewAPIServer(tracingEnabled bool) Server { s := &apiServer{} - mainRouter := s.routes() + mainRouter := s.routes(tracingEnabled) // referring to the router as type http.Handler allows us to add middleware via more handlers var mainHandler http.Handler = mainRouter diff --git a/cmd/hyperfleet-api/server/routes.go b/cmd/hyperfleet-api/server/routes.go index bd3e907..b078b94 100755 --- a/cmd/hyperfleet-api/server/routes.go +++ b/cmd/hyperfleet-api/server/routes.go @@ -47,7 +47,7 @@ func LoadDiscoveredRoutes( } } -func (s *apiServer) routes() *mux.Router { +func (s *apiServer) routes(tracingEnabled bool) *mux.Router { services := &env().Services metadataHandler := handlers.NewMetadataHandler() @@ -79,7 +79,7 @@ func (s *apiServer) routes() *mux.Router { // OpenTelemetry middleware (conditionally enabled) // Extracts trace_id/span_id from traceparent header and adds to logger context - if env().Config.Logging.OTel.Enabled { + if tracingEnabled { mainRouter.Use(middleware.OTelMiddleware) } diff --git a/docs/config.md b/docs/config.md index 0eca514..74c2508 100644 --- a/docs/config.md +++ b/docs/config.md @@ -70,6 +70,16 @@ export HYPERFLEET_DATABASE_PASSWORD=secret-password # Result: Uses "secret-password" (env var wins) ``` +**Special Case - OpenTelemetry Tracing:** + +`TRACING_ENABLED` (Tracing standard) has special precedence for cross-component consistency: + +```text +TRACING_ENABLED > config (env/flags) > default +``` + +See [OpenTelemetry Configuration](#opentelemetry-configuration) for details. + --- ## Configuration File Locations @@ -180,8 +190,7 @@ Logging behavior and output settings. | `logging.level` | string | `info` | Log level: `debug`, `info`, `warn`, `error` | | `logging.format` | string | `json` | Log format: `json`, `text` | | `logging.output` | string | `stdout` | Log output: `stdout`, `stderr` | -| `logging.otel.enabled` | bool | `false` | Enable OpenTelemetry tracing | -| `logging.otel.sampling_rate` | float | `1.0` | OTEL sampling rate (0.0-1.0) | +| `logging.otel.enabled` | bool | `true` | Enable OpenTelemetry tracing (see [OpenTelemetry Configuration](#opentelemetry-configuration)) | | `logging.masking.enabled` | bool | `true` | Enable sensitive data masking in logs | **Example:** @@ -200,6 +209,31 @@ logging: - token ``` +### OpenTelemetry Configuration + +OpenTelemetry tracing is configured via standard environment variables following the [HyperFleet Tracing Standard](https://github.com/openshift-hyperfleet/architecture/blob/main/hyperfleet/standards/tracing.md). + +**Enabling Tracing:** + +| Property | Environment Variable | Type | Default | Description | +|----------|---------------------|------|---------|-------------| +| `logging.otel.enabled` | `TRACING_ENABLED` | bool | `true` | Enable OpenTelemetry tracing (HyperFleet standard) | + +**Standard OpenTelemetry Environment Variables:** + +Once enabled, tracing is configured using standard OpenTelemetry variables: + +| Variable | Description | Default | +|----------|-------------|---------| +| `OTEL_SERVICE_NAME` | Service name in traces | `hyperfleet-api` | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint | stdout exporter | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | Export protocol (`grpc` or `http/protobuf`) | `grpc` | +| `OTEL_TRACES_SAMPLER` | Sampler type | `parentbased_traceidratio` | +| `OTEL_TRACES_SAMPLER_ARG` | Sampling rate (0.0-1.0) | `1.0` | +| `OTEL_RESOURCE_ATTRIBUTES` | Additional resource attributes (k=v,k2=v2) | - | + +**See:** [Logging Documentation](logging.md#opentelemetry-integration) for tracing configuration details and [Tracing Standard](https://github.com/openshift-hyperfleet/architecture/blob/main/hyperfleet/standards/tracing.md#configuration) for complete reference. + --- ## Advanced Configuration @@ -360,8 +394,7 @@ Complete table of all configuration properties, their environment variables, and | `logging.level` | `HYPERFLEET_LOGGING_LEVEL` | string | `info` | | `logging.format` | `HYPERFLEET_LOGGING_FORMAT` | string | `json` | | `logging.output` | `HYPERFLEET_LOGGING_OUTPUT` | string | `stdout` | -| `logging.otel.enabled` | `HYPERFLEET_LOGGING_OTEL_ENABLED` | bool | `false` | -| `logging.otel.sampling_rate` | `HYPERFLEET_LOGGING_OTEL_SAMPLING_RATE` | float | `1.0` | +| `logging.otel.enabled` | `TRACING_ENABLED` | bool | `true` | | `logging.masking.enabled` | `HYPERFLEET_LOGGING_MASKING_ENABLED` | bool | `true` | | `logging.masking.headers` | `HYPERFLEET_LOGGING_MASKING_HEADERS` | csv | `Authorization,Cookie` | | `logging.masking.fields` | `HYPERFLEET_LOGGING_MASKING_FIELDS` | csv | `password,token` | @@ -422,8 +455,6 @@ All CLI flags and their corresponding configuration paths. | `--log-level`, `-l` | `logging.level` | string | | `--log-format` | `logging.format` | string | | `--log-output` | `logging.output` | string | -| `--log-otel-enabled` | `logging.otel.enabled` | bool | -| `--log-otel-sampling-rate` | `logging.otel.sampling_rate` | float | | **OCM** | | | | `--ocm-base-url` | `ocm.base_url` | string | | `--ocm-client-id` | `ocm.client_id` | string | @@ -507,7 +538,6 @@ The application performs comprehensive validation at startup. **Logging**: - `logging.level`: must be `debug`, `info`, `warn`, or `error` - `logging.format`: must be `json` or `text` -- `logging.otel.sampling_rate`: 0.0-1.0 **Adapters**: - `adapters.required.cluster`: must be array of strings @@ -611,3 +641,5 @@ Before deploying to production, verify: - ✅ CLI flags (--kebab-case) - ✅ Configuration files (YAML snake_case) - ✅ Default values +- ✅ OpenTelemetry tracing variables (TRACING_ENABLED, OTEL_*) if tracing is enabled + diff --git a/docs/logging.md b/docs/logging.md index e400720..438f7f2 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -54,13 +54,36 @@ export HYPERFLEET_LOGGING_LEVEL=debug # Structured JSON format with info level export HYPERFLEET_LOGGING_FORMAT=json export HYPERFLEET_LOGGING_LEVEL=info -export HYPERFLEET_LOGGING_OTEL_ENABLED=true -export HYPERFLEET_LOGGING_OTEL_SAMPLING_RATE=0.1 + +# OpenTelemetry tracing (Tracing standard) +export TRACING_ENABLED=true +export OTEL_TRACES_SAMPLER=parentbased_traceidratio +export OTEL_TRACES_SAMPLER_ARG=0.1 +export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 ``` **For complete configuration reference**, including all logging settings (levels, formats, OpenTelemetry, masking), see: - **[Configuration Guide](config.md)** - All logging environment variables and defaults +### OpenTelemetry Environment Variables + +HyperFleet uses standard OpenTelemetry environment variables for tracing configuration: + +| Variable | Description | Default | Example | +|----------|-------------|---------|---------| +| `TRACING_ENABLED` | Enable/disable tracing (Tracing standard, overrides config) | - | `true`, `false` | +| `OTEL_SERVICE_NAME` | Service name in traces | `hyperfleet-api` | `hyperfleet-api-prod` | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint (if not set, uses stdout) | - | `http://otel-collector:4317` | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol | `grpc` | `grpc`, `http/protobuf` | +| `OTEL_TRACES_SAMPLER` | Sampler type | `parentbased_traceidratio` | `always_on`, `traceidratio` | +| `OTEL_TRACES_SAMPLER_ARG` | Sampling rate (0.0-1.0) | `1.0` | `0.1` (10%) | +| `OTEL_RESOURCE_ATTRIBUTES` | Additional resource attributes | - | `env=prod,region=us-east` | + +**Variable Precedence (highest to lowest):** +1. `TRACING_ENABLED` - Tracing standard (env var) +2. `config.yaml: logging.otel.enabled` - Config file +3. Default (`true`) + ## Usage ### Basic Logging @@ -346,24 +369,16 @@ logger.With(ctx, "host", "postgres.svc").WithError(err).Error("Failed to connect ### Initialization -OpenTelemetry is initialized in `cmd/hyperfleet-api/servecmd/cmd.go`: +OpenTelemetry is initialized in `cmd/hyperfleet-api/servecmd/cmd.go` (see `runServe()` function, lines ~74-110). -```go -if environments.Environment().Config.Logging.OTel.Enabled { - samplingRate := environments.Environment().Config.Logging.OTel.SamplingRate - tp, err := telemetry.InitTraceProvider(ctx, "hyperfleet-api", api.Version, samplingRate) - if err != nil { - logger.WithError(ctx, err).Warn("Failed to initialize OpenTelemetry") - } else { - defer func() { - if err := tp.Shutdown(context.Background()); err != nil { - logger.WithError(ctx, err).Error("Error shutting down tracer provider") - } - }() - logger.With(ctx, logger.FieldSamplingRate, samplingRate).Info("OpenTelemetry initialized") - } -} -``` +**Key behavior:** +- Checks `TRACING_ENABLED` environment variable first (tracing standard) +- Falls back to config file setting if not set +- Uses `OTEL_SERVICE_NAME` if set, otherwise defaults to `"hyperfleet-api"` +- Initializes trace provider via `telemetry.InitTraceProvider(ctx, serviceName, api.Version)` +- Shuts down with timeout during graceful shutdown + +See the actual implementation for complete error handling and shutdown logic. ### Trace Propagation @@ -375,10 +390,26 @@ The OTel middleware automatically: ### Sampling -Configure sampling rate to control trace volume: +Configure sampling using standard OpenTelemetry environment variables: + +```bash +# Sampler type (default: parentbased_traceidratio) +export OTEL_TRACES_SAMPLER=parentbased_traceidratio + +# Sampling rate: 0.0-1.0 (default: 1.0) +export OTEL_TRACES_SAMPLER_ARG=0.1 # 10% of requests traced +``` + +**Sampling rate examples:** - `0.0`: No traces (disabled) -- `0.1`: 10% of requests traced -- `1.0`: All requests traced (use in development only) +- `0.1`: 10% of requests traced (recommended for production) +- `1.0`: All requests traced (development only) + +**Sampler types:** +- `always_on`: Sample all requests +- `always_off`: Sample no requests +- `traceidratio`: Sample based on trace ID ratio (use with OTEL_TRACES_SAMPLER_ARG) +- `parentbased_traceidratio`: Respect parent decision, otherwise use trace ID ratio (default) ## Data Masking @@ -443,9 +474,9 @@ mainRouter.Use(logging.RequestLoggingMiddleware) ### Missing trace_id/span_id -1. Check OTel is enabled: `export HYPERFLEET_LOGGING_OTEL_ENABLED=true` +1. Check tracing is enabled: `export TRACING_ENABLED=true` 2. Verify middleware order: `OTelMiddleware` must be after `RequestIDMiddleware` -3. Check sampling rate: `export HYPERFLEET_LOGGING_OTEL_SAMPLING_RATE=1.0` (for testing) +3. Check sampling rate: `export OTEL_TRACES_SAMPLER_ARG=1.0` (for testing - trace all requests) ### Data Not Masked @@ -502,7 +533,7 @@ func TestLogging(t *testing.T) { HYPERFLEET_LOGGING_LEVEL=debug OCM_ENV=integration_testing go test ./test/integration/... # Run tests without OTel -HYPERFLEET_LOGGING_OTEL_ENABLED=false OCM_ENV=integration_testing go test ./... +TRACING_ENABLED=false OCM_ENV=integration_testing go test ./... ``` ## References diff --git a/go.mod b/go.mod index 20de71b..863c15f 100755 --- a/go.mod +++ b/go.mod @@ -32,10 +32,12 @@ require ( github.com/testcontainers/testcontainers-go/modules/postgres v0.33.0 github.com/yaacov/tree-search-language v0.0.0-20190923184055-1c2dad2e354b go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 - go.opentelemetry.io/otel v1.38.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 - go.opentelemetry.io/otel/sdk v1.38.0 - go.opentelemetry.io/otel/trace v1.38.0 + go.opentelemetry.io/otel 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/exporters/stdout/stdouttrace v1.40.0 + go.opentelemetry.io/otel/sdk v1.40.0 + go.opentelemetry.io/otel/trace v1.40.0 go.uber.org/mock v0.6.0 gopkg.in/resty.v1 v1.12.0 gorm.io/datatypes v1.2.7 @@ -53,6 +55,7 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -80,6 +83,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/gorilla/css v1.0.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -133,17 +137,19 @@ require ( github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/metric 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/crypto v0.46.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/protobuf v1.36.10 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.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/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.5.6 // indirect ) diff --git a/go.sum b/go.sum index 06a188f..a613c5f 100755 --- a/go.sum +++ b/go.sum @@ -272,8 +272,8 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/graphql-go/graphql v0.7.8/go.mod h1:k6yrAYQaSP59DC5UVxbgxESlmVyojThKdORUqGDGmrI= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +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/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hokaccha/go-prettyjson v0.0.0-20180920040306-f579f869bbfe/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= @@ -644,24 +644,26 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= -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/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= -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/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= -go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +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/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/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/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/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -696,8 +698,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -775,8 +777,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +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.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -860,13 +862,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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/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-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -875,8 +877,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +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.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -938,6 +940,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T 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/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -995,10 +999,10 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +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.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -1013,8 +1017,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +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 v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1027,8 +1031,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -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/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/config/flags.go b/pkg/config/flags.go index 1b79d5d..d7a7600 100644 --- a/pkg/config/flags.go +++ b/pkg/config/flags.go @@ -60,8 +60,6 @@ func AddLoggingFlags(cmd *cobra.Command) { cmd.Flags().StringP("log-level", "l", defaults.Level, "Log level (debug, info, warn, error)") cmd.Flags().StringP("log-format", "f", defaults.Format, "Log format (json, text)") cmd.Flags().String("log-output", defaults.Output, "Log output (stdout, stderr)") - cmd.Flags().Bool("log-otel-enabled", defaults.OTel.Enabled, "Enable OpenTelemetry tracing") - cmd.Flags().Float64("log-otel-sampling-rate", defaults.OTel.SamplingRate, "OpenTelemetry sampling rate (0.0-1.0)") cmd.Flags().Bool("log-masking-enabled", defaults.Masking.Enabled, "Enable log masking for sensitive data") cmd.Flags().String("log-masking-sensitive-headers", defaults.GetSensitiveHeadersString(), "Comma-separated list of sensitive HTTP headers to mask") diff --git a/pkg/config/loader.go b/pkg/config/loader.go index dc6c324..f2b4066 100644 --- a/pkg/config/loader.go +++ b/pkg/config/loader.go @@ -327,8 +327,6 @@ func (l *ConfigLoader) bindAllEnvVars() { l.bindEnv("logging.level") l.bindEnv("logging.format") l.bindEnv("logging.output") - l.bindEnv("logging.otel.enabled") - l.bindEnv("logging.otel.sampling_rate") l.bindEnv("logging.masking.enabled") l.bindEnv("logging.masking.headers") l.bindEnv("logging.masking.fields") @@ -401,8 +399,6 @@ func (l *ConfigLoader) bindFlags(cmd *cobra.Command) { l.bindPFlag("logging.level", cmd.Flags().Lookup("log-level")) l.bindPFlag("logging.format", cmd.Flags().Lookup("log-format")) l.bindPFlag("logging.output", cmd.Flags().Lookup("log-output")) - l.bindPFlag("logging.otel.enabled", cmd.Flags().Lookup("log-otel-enabled")) - l.bindPFlag("logging.otel.sampling_rate", cmd.Flags().Lookup("log-otel-sampling-rate")) l.bindPFlag("logging.masking.enabled", cmd.Flags().Lookup("log-masking-enabled")) l.bindPFlag("logging.masking.headers", cmd.Flags().Lookup("log-masking-sensitive-headers")) l.bindPFlag("logging.masking.fields", cmd.Flags().Lookup("log-masking-sensitive-fields")) diff --git a/pkg/config/loader_test.go b/pkg/config/loader_test.go index d097300..8f75b7c 100644 --- a/pkg/config/loader_test.go +++ b/pkg/config/loader_test.go @@ -399,7 +399,6 @@ func TestConfigLoader_FlagParsing(t *testing.T) { cmd.Flags().Set("server-read-timeout", "10s") //nolint:errcheck,gosec // duration cmd.Flags().Set("server-jwt-enabled", "false") //nolint:errcheck,gosec // bool cmd.Flags().Set("db-max-open-connections", "50") //nolint:errcheck,gosec // int - cmd.Flags().Set("log-otel-sampling-rate", "0.5") //nolint:errcheck,gosec // float64 ctx := context.Background() cfg, err := loader.Load(ctx, cmd) @@ -411,5 +410,4 @@ func TestConfigLoader_FlagParsing(t *testing.T) { Expect(cfg.Server.Timeouts.Read.Seconds()).To(Equal(float64(10)), "duration parsing") Expect(cfg.Server.JWT.Enabled).To(BeFalse(), "bool parsing") Expect(cfg.Database.Pool.MaxConnections).To(Equal(50), "int parsing") - Expect(cfg.Logging.OTel.SamplingRate).To(Equal(0.5), "float64 parsing") } diff --git a/pkg/config/logging.go b/pkg/config/logging.go index 09638f7..dad1b18 100644 --- a/pkg/config/logging.go +++ b/pkg/config/logging.go @@ -15,9 +15,10 @@ type LoggingConfig struct { } // OTelConfig holds OpenTelemetry configuration +// Configuration is driven entirely by standard environment variables. +// See: https://github.com/openshift-hyperfleet/architecture/blob/main/hyperfleet/standards/tracing.md#configuration type OTelConfig struct { - Enabled bool `mapstructure:"enabled" json:"enabled"` - SamplingRate float64 `mapstructure:"sampling_rate" json:"sampling_rate" validate:"gte=0,lte=1"` + Enabled bool `mapstructure:"enabled" json:"enabled"` } // MaskingConfig holds log masking configuration @@ -35,8 +36,7 @@ func NewLoggingConfig() *LoggingConfig { Format: "json", Output: "stdout", OTel: OTelConfig{ - Enabled: false, - SamplingRate: 1.0, + Enabled: true, }, Masking: MaskingConfig{ Enabled: true, diff --git a/pkg/config/logging_test.go b/pkg/config/logging_test.go index 1f9619b..65c6f27 100644 --- a/pkg/config/logging_test.go +++ b/pkg/config/logging_test.go @@ -18,8 +18,7 @@ func TestNewLoggingConfig_Defaults(t *testing.T) { Expect(cfg.Level).To(Equal("info")) Expect(cfg.Format).To(Equal("json")) Expect(cfg.Output).To(Equal("stdout")) - Expect(cfg.OTel.Enabled).To(BeFalse()) - Expect(cfg.OTel.SamplingRate).To(Equal(1.0)) + Expect(cfg.OTel.Enabled).To(BeTrue()) Expect(cfg.Masking.Enabled).To(BeTrue()) Expect(cfg.Masking.Headers).NotTo(BeEmpty()) Expect(cfg.Masking.Fields).NotTo(BeEmpty()) @@ -33,8 +32,6 @@ func TestConfigLoader_LoggingFromEnv(t *testing.T) { t.Setenv("HYPERFLEET_LOGGING_LEVEL", "debug") t.Setenv("HYPERFLEET_LOGGING_FORMAT", "text") - t.Setenv("HYPERFLEET_LOGGING_OTEL_ENABLED", "true") - t.Setenv("HYPERFLEET_LOGGING_OTEL_SAMPLING_RATE", "0.5") loader := NewConfigLoader() cmd := &cobra.Command{} @@ -45,8 +42,8 @@ func TestConfigLoader_LoggingFromEnv(t *testing.T) { Expect(err).NotTo(HaveOccurred()) Expect(appConfig.Logging.Level).To(Equal("debug")) Expect(appConfig.Logging.Format).To(Equal("text")) + // OTel.Enabled defaults to true Expect(appConfig.Logging.OTel.Enabled).To(BeTrue()) - Expect(appConfig.Logging.OTel.SamplingRate).To(Equal(0.5)) } // TestLoggingConfig_GetSensitiveHeadersList tests the headers array accessor diff --git a/pkg/logger/fields.go b/pkg/logger/fields.go index 8c0d45f..533e5dd 100644 --- a/pkg/logger/fields.go +++ b/pkg/logger/fields.go @@ -42,6 +42,11 @@ const ( FieldOTelEnabled = "otel_enabled" FieldSamplingRate = "sampling_rate" FieldExporterEndpoint = "exporter_endpoint" + FieldTracingEnabled = "tracing_enabled" + FieldServiceName = "service_name" + FieldProtocol = "protocol" + FieldSampler = "sampler" + FieldServiceVersion = "service_version" ) // Schema related fields diff --git a/pkg/telemetry/otel.go b/pkg/telemetry/otel.go index 73a8b28..3e5ab77 100644 --- a/pkg/telemetry/otel.go +++ b/pkg/telemetry/otel.go @@ -2,9 +2,16 @@ package telemetry import ( "context" + "fmt" + "os" + "strconv" + "strings" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.37.0" @@ -12,45 +19,99 @@ import ( "github.com/openshift-hyperfleet/hyperfleet-api/pkg/logger" ) +const ( + samplerAlwaysOn = "always_on" + samplerAlwaysOff = "always_off" + samplerTraceIDRatio = "traceidratio" + envOtelTracesSampler = "OTEL_TRACES_SAMPLER" + envOtelTracesSamplerArg = "OTEL_TRACES_SAMPLER_ARG" + envOtelExporterOtlpEndpoint = "OTEL_EXPORTER_OTLP_ENDPOINT" + envOtelExporterOtlpProtocol = "OTEL_EXPORTER_OTLP_PROTOCOL" + parentBasedTraceIDRatio = "parentbased_traceidratio" + parentBasedAlwaysOn = "parentbased_always_on" + parentBasedAlwaysOff = "parentbased_always_off" + defaultSamplingRate = 1.0 +) + // InitTraceProvider initializes OpenTelemetry trace provider -// Uses stdout exporter (traces output to logs, no external Collector needed) -// Future upgrade: Switch to OTLP HTTP exporter by changing only the exporter creation -func InitTraceProvider( - ctx context.Context, serviceName, serviceVersion string, samplingRate float64, -) (*trace.TracerProvider, error) { - // Create stdout exporter - exporter, err := stdouttrace.New( - stdouttrace.WithPrettyPrint(), // Formatted output - ) - if err != nil { - logger.WithError(ctx, err).Error("Failed to create OpenTelemetry stdout exporter") - return nil, err +// Configuration is driven entirely by standard OpenTelemetry environment variables: +// - OTEL_EXPORTER_OTLP_ENDPOINT: OTLP collector endpoint (if not set, uses stdout) +// - OTEL_EXPORTER_OTLP_PROTOCOL: "grpc" (default) or "http/protobuf" +// - OTEL_TRACES_SAMPLER: sampler type (default: "parentbased_traceidratio") +// - OTEL_TRACES_SAMPLER_ARG: sampling rate 0.0-1.0 (default: 1.0) +// - OTEL_RESOURCE_ATTRIBUTES: additional resource attributes (k=v,k2=v2 format) +func InitTraceProvider(ctx context.Context, serviceName, serviceVersion string) (*trace.TracerProvider, error) { + + var exporter trace.SpanExporter + var err error + + if otlpEndpoint := os.Getenv(envOtelExporterOtlpEndpoint); otlpEndpoint != "" { + protocol := os.Getenv(envOtelExporterOtlpProtocol) + switch strings.ToLower(protocol) { + case "http/protobuf": + // Note: http/json not yet supported - use http/protobuf + exporter, err = otlptracehttp.New(ctx) + case "grpc", "": // Default to gRPC per standard + exporter, err = otlptracegrpc.New(ctx) + default: + // Spec-compliant values: grpc, http/protobuf + logger.With(ctx, logger.FieldProtocol, protocol).Warn("Unrecognized OTEL_EXPORTER_OTLP_PROTOCOL, using default grpc") + exporter, err = otlptracegrpc.New(ctx) + } + if err != nil { + logger.With(ctx, logger.FieldProtocol, protocol).WithError(err).Error("Failed to create OTLP exporter") + return nil, fmt.Errorf("failed to create OTLP exporter (protocol=%s): %w", protocol, err) + } + } else { + // Create stdout exporter + exporter, err = stdouttrace.New( + stdouttrace.WithPrettyPrint(), // Formatted output + ) + if err != nil { + logger.WithError(ctx, err).Error("Failed to create OpenTelemetry stdout exporter") + return nil, fmt.Errorf("failed to create OpenTelemetry stdout exporter: %w", err) + } } // Create resource (service information) res, err := resource.New(ctx, + resource.WithFromEnv(), // parse OTEL_RESOURCE_ATTRIBUTES resource.WithAttributes( semconv.ServiceNameKey.String(serviceName), semconv.ServiceVersionKey.String(serviceVersion), ), ) if err != nil { + if shutdownErr := exporter.Shutdown(ctx); shutdownErr != nil { + logger.WithError(ctx, shutdownErr).Warn("Failed to shutdown exporter") + } logger.With(ctx, - "service_name", serviceName, - "service_version", serviceVersion, + logger.FieldServiceName, serviceName, + logger.FieldServiceVersion, serviceVersion, ).WithError(err).Error("Failed to create OpenTelemetry resource") - return nil, err + return nil, fmt.Errorf("failed to create OTel resource: %w", err) } - // Determine sampler based on sampling rate var sampler trace.Sampler - switch { - case samplingRate >= 1.0: - sampler = trace.AlwaysSample() // Sample all - case samplingRate <= 0.0: - sampler = trace.NeverSample() // Sample none + samplerType := strings.ToLower(os.Getenv(envOtelTracesSampler)) + + switch samplerType { + case samplerAlwaysOn: + sampler = trace.AlwaysSample() + case samplerAlwaysOff: + sampler = trace.NeverSample() + case samplerTraceIDRatio: + sampler = trace.TraceIDRatioBased(parseSamplingRate(ctx)) + case parentBasedTraceIDRatio, "": + // Default per tracing standard + sampler = trace.ParentBased(trace.TraceIDRatioBased(parseSamplingRate(ctx))) + case parentBasedAlwaysOn: + sampler = trace.ParentBased(trace.AlwaysSample()) + case parentBasedAlwaysOff: + sampler = trace.ParentBased(trace.NeverSample()) default: - sampler = trace.TraceIDRatioBased(samplingRate) + logger.With(ctx, logger.FieldSampler, samplerType).Warn("Unrecognized sampler, using default") + sampler = trace.ParentBased(trace.TraceIDRatioBased(parseSamplingRate(ctx))) } // Create trace provider @@ -62,6 +123,10 @@ func InitTraceProvider( // Set global trace provider otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) return tp, nil } @@ -73,3 +138,17 @@ func Shutdown(ctx context.Context, tp *trace.TracerProvider) error { } return tp.Shutdown(ctx) } + +// parseSamplingRate parses sampling rate from OTEL_TRACES_SAMPLER_ARG environment variable +func parseSamplingRate(ctx context.Context) float64 { + rate := defaultSamplingRate + if arg := os.Getenv(envOtelTracesSamplerArg); arg != "" { + if parsedRate, err := strconv.ParseFloat(arg, 64); err == nil && parsedRate >= 0.0 && parsedRate <= 1.0 { + rate = parsedRate + } else { + logger.With(ctx, logger.FieldSamplingRate, rate, "raw_value", arg). + Warn("Invalid OTEL_TRACES_SAMPLER_ARG value, using default") + } + } + return rate +} diff --git a/pkg/telemetry/otel_test.go b/pkg/telemetry/otel_test.go new file mode 100644 index 0000000..c7e1604 --- /dev/null +++ b/pkg/telemetry/otel_test.go @@ -0,0 +1,361 @@ +package telemetry + +import ( + "context" + "testing" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/sdk/trace" +) + +func TestInitTraceProvider_StdoutExporter(t *testing.T) { + ctx := context.Background() + + // Test stdout exporter (default) + tp, err := InitTraceProvider(ctx, "test-service", "v1.0.0") + if err != nil { + t.Fatalf("Failed to initialize trace provider: %v", err) + } + if tp == nil { + t.Fatal("Expected trace provider, got nil") + } + + // Cleanup + defer func() { + if err := Shutdown(ctx, tp); err != nil { + t.Errorf("Failed to shutdown trace provider: %v", err) + } + }() + + // Verify tracer is available + tracer := otel.Tracer("test") + if tracer == nil { + t.Error("Expected tracer to be available") + } +} + +func TestInitTraceProvider_OTLPExporter(t *testing.T) { + ctx := context.Background() + + // Set OTLP endpoint (can be fake - we're just testing initialization) + t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://fake-otel-collector:4317") + + // Test that trace provider initializes correctly with OTLP exporter + tp, err := InitTraceProvider(ctx, "test-service", "v1.0.0") + if err != nil { + t.Fatalf("Failed to initialize trace provider with OTLP: %v", err) + } + if tp == nil { + t.Fatal("Expected trace provider, got nil") + } + defer func() { + if err := Shutdown(ctx, tp); err != nil { + t.Errorf("Failed to shutdown trace provider: %v", err) + } + }() + + // Verify tracer is available + tracer := otel.Tracer("test") + if tracer == nil { + t.Error("Expected tracer to be available") + } + +} + +func TestInitTraceProvider_InvalidProtocol(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + protocol string + }{ + { + name: "bare_http_is_ambiguous", + protocol: "http", + }, + { + name: "http_json_not_yet_supported", + protocol: "http/json", + }, + { + name: "invalid_protocol", + protocol: "invalid", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://fake-otel-collector:4317") + t.Setenv("OTEL_EXPORTER_OTLP_PROTOCOL", tt.protocol) + + // Should fall back to gRPC with a warning (not fail) + tp, err := InitTraceProvider(ctx, "test-service", "v1.0.0") + if err != nil { + t.Fatalf("Failed to initialize trace provider: %v", err) + } + if tp == nil { + t.Fatal("Expected trace provider, got nil") + } + defer func() { + if err := Shutdown(ctx, tp); err != nil { + t.Errorf("Failed to shutdown trace provider: %v", err) + } + }() + + // Verify tracer is available (using default gRPC exporter) + tracer := otel.Tracer("test") + if tracer == nil { + t.Error("Expected tracer to be available") + } + }) + } +} + +func TestInitTraceProvider_SamplerEnvironmentVariables(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + samplerType string + samplerArg string + expectedSample bool + }{ + { + name: "always_on", + samplerType: "always_on", + expectedSample: true, + }, + { + name: "always_off", + samplerType: "always_off", + expectedSample: false, + }, + { + name: "traceidratio_high", + samplerType: "traceidratio", + samplerArg: "1.0", + expectedSample: true, + }, + { + name: "traceidratio_zero", + samplerType: "traceidratio", + samplerArg: "0.0", + expectedSample: false, + }, + { + name: "parentbased_traceidratio_default", + samplerType: "parentbased_traceidratio", + samplerArg: "1.0", + expectedSample: true, + }, + { + name: "parentbased_always_on", + samplerType: "parentbased_always_on", + expectedSample: true, + }, + { + name: "parentbased_always_off", + samplerType: "parentbased_always_off", + expectedSample: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set environment variables + if tt.samplerType != "" { + t.Setenv("OTEL_TRACES_SAMPLER", tt.samplerType) + } + if tt.samplerArg != "" { + t.Setenv("OTEL_TRACES_SAMPLER_ARG", tt.samplerArg) + } + + tp, err := InitTraceProvider(ctx, "test-service", "v1.0.0") + if err != nil { + t.Fatalf("Failed to initialize trace provider: %v", err) + } + defer func(ctx context.Context, tp *trace.TracerProvider) { + err := Shutdown(ctx, tp) + if err != nil { + t.Errorf("Failed to shutdown trace provider: %v", err) + } + }(ctx, tp) + + // Test sampling behavior by checking if spans are created + tracer := otel.Tracer("test") + _, span := tracer.Start(ctx, "test-span") + + if tt.expectedSample { + if !span.SpanContext().IsValid() || !span.SpanContext().TraceFlags().IsSampled() { + t.Error("Expected valid and sampled span context for sampling=true") + } + } else { + // Verify span is NOT sampled for expectedSample=false + if span.SpanContext().IsValid() && span.SpanContext().TraceFlags().IsSampled() { + t.Error("Expected span to NOT be sampled for sampling=false") + } + } + span.End() + }) + } +} + +func TestInitTraceProvider_InvalidSamplerArg(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + samplerArg string + expectedSample bool // Should fall back to default (1.0 = always sample) + }{ + { + name: "negative_value", + samplerArg: "-1.0", + expectedSample: true, // Falls back to default 1.0 + }, + { + name: "above_one", + samplerArg: "2.0", + expectedSample: true, // Falls back to default 1.0 + }, + { + name: "non_numeric", + samplerArg: "invalid", + expectedSample: true, // Falls back to default 1.0 + }, + { + name: "empty_string", + samplerArg: "", + expectedSample: true, // Falls back to default 1.0 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Use parentbased_traceidratio to test the sampling rate parsing + t.Setenv("OTEL_TRACES_SAMPLER", "parentbased_traceidratio") + + if tt.samplerArg != "" { + t.Setenv("OTEL_TRACES_SAMPLER_ARG", tt.samplerArg) + } + + tp, err := InitTraceProvider(ctx, "test-service", "v1.0.0") + if err != nil { + t.Fatalf("Failed to initialize trace provider: %v", err) + } + defer func(ctx context.Context, tp *trace.TracerProvider) { + err := Shutdown(ctx, tp) + if err != nil { + t.Errorf("Failed to shutdown trace provider: %v", err) + } + }(ctx, tp) + + // Test that invalid values fall back to default (1.0 = always sample) + tracer := otel.Tracer("test") + _, span := tracer.Start(ctx, "test-span") + + if tt.expectedSample { + if !span.SpanContext().IsValid() || !span.SpanContext().TraceFlags().IsSampled() { + t.Errorf("Expected span to be sampled (fallback to default 1.0) for invalid arg %q", tt.samplerArg) + } + } + span.End() + }) + } +} + +func TestInitTraceProvider_ParentBasedSampling(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + samplerType string + samplerArg string + withParent bool + expectedSample bool + }{ + { + name: "root_span_with_ratio_high", + samplerType: "parentbased_traceidratio", + samplerArg: "1.0", + withParent: false, + expectedSample: true, // Root span uses ratio (1.0 = sample) + }, + { + name: "child_span_inherits_parent_sampling", + samplerType: "parentbased_traceidratio", + samplerArg: "1.0", + withParent: true, + expectedSample: true, // Parent sampled, child follows + }, + { + name: "root_span_with_ratio_zero", + samplerType: "parentbased_traceidratio", + samplerArg: "0.0", + withParent: false, + expectedSample: false, // Root span uses ratio (0.0 = don't sample) + }, + { + name: "root_span_always_on", + samplerType: "parentbased_always_on", + samplerArg: "", + withParent: false, + expectedSample: true, // Root span always sampled + }, + { + name: "root_span_always_off", + samplerType: "parentbased_always_off", + samplerArg: "", + withParent: false, + expectedSample: false, // Root span never sampled + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("OTEL_TRACES_SAMPLER", tt.samplerType) + + if tt.samplerArg != "" { + t.Setenv("OTEL_TRACES_SAMPLER_ARG", tt.samplerArg) + } + + tp, err := InitTraceProvider(ctx, "test-service", "v1.0.0") + if err != nil { + t.Fatalf("Failed to initialize trace provider: %v", err) + } + defer func(ctx context.Context, tp *trace.TracerProvider) { + err := Shutdown(ctx, tp) + if err != nil { + t.Errorf("Failed to shutdown trace provider: %v", err) + } + }(ctx, tp) + + tracer := otel.Tracer("test") + + var testCtx context.Context + if tt.withParent { + // Create parent span and use its context + parentCtx, parentSpan := tracer.Start(ctx, "parent-span") + testCtx = parentCtx + defer parentSpan.End() + } else { + // Root span (no parent) + testCtx = ctx + } + + // Create test span (root or child depending on withParent) + _, span := tracer.Start(testCtx, "test-span") + + if tt.expectedSample { + if !span.SpanContext().IsValid() || !span.SpanContext().TraceFlags().IsSampled() { + t.Errorf("Expected span to be sampled for %s", tt.name) + } + } else { + if span.SpanContext().IsValid() && span.SpanContext().TraceFlags().IsSampled() { + t.Errorf("Expected span to NOT be sampled for %s", tt.name) + } + } + span.End() + }) + } +} diff --git a/test/helper.go b/test/helper.go index 055fc36..da3e336 100755 --- a/test/helper.go +++ b/test/helper.go @@ -161,7 +161,8 @@ func (helper *Helper) startAPIServer() { ctx := context.Background() // Configure JWK certificate URL for API server helper.Env().Config.Server.JWK.CertURL = jwkURL - helper.APIServer = server.NewAPIServer() + // Disable tracing for integration tests (no OTLP collector required) + helper.APIServer = server.NewAPIServer(false) listener, err := helper.APIServer.Listen() if err != nil { logger.WithError(ctx, err).Error("Unable to start Test API server")