diff --git a/backend/v3/instrumentation/exportertype_enumer.go b/backend/v3/instrumentation/exportertype_enumer.go index 3f9d15b7b..d8f2f46b7 100644 --- a/backend/v3/instrumentation/exportertype_enumer.go +++ b/backend/v3/instrumentation/exportertype_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _ExporterTypeName = "NoneStdOutStdErrGRPCHTTPGooglePrometheus" +const _ExporterTypeName = "NoneStdOutStdErrGRPCHTTPGooglePrometheusAuto" -var _ExporterTypeIndex = [...]uint8{0, 0, 4, 10, 16, 20, 24, 30, 40} +var _ExporterTypeIndex = [...]uint8{0, 0, 4, 10, 16, 20, 24, 30, 40, 44} -const _ExporterTypeLowerName = "nonestdoutstderrgrpchttpgoogleprometheus" +const _ExporterTypeLowerName = "nonestdoutstderrgrpchttpgoogleprometheusauto" func (i ExporterType) String() string { if i < 0 || i >= ExporterType(len(_ExporterTypeIndex)-1) { @@ -32,9 +32,10 @@ func _ExporterTypeNoOp() { _ = x[ExporterTypeHTTP-(5)] _ = x[ExporterTypeGoogle-(6)] _ = x[ExporterTypePrometheus-(7)] + _ = x[ExporterTypeAuto-(8)] } -var _ExporterTypeValues = []ExporterType{ExporterTypeUnspecified, ExporterTypeNone, ExporterTypeStdOut, ExporterTypeStdErr, ExporterTypeGRPC, ExporterTypeHTTP, ExporterTypeGoogle, ExporterTypePrometheus} +var _ExporterTypeValues = []ExporterType{ExporterTypeUnspecified, ExporterTypeNone, ExporterTypeStdOut, ExporterTypeStdErr, ExporterTypeGRPC, ExporterTypeHTTP, ExporterTypeGoogle, ExporterTypePrometheus, ExporterTypeAuto} var _ExporterTypeNameToValueMap = map[string]ExporterType{ _ExporterTypeName[0:0]: ExporterTypeUnspecified, @@ -53,6 +54,8 @@ var _ExporterTypeNameToValueMap = map[string]ExporterType{ _ExporterTypeLowerName[24:30]: ExporterTypeGoogle, _ExporterTypeName[30:40]: ExporterTypePrometheus, _ExporterTypeLowerName[30:40]: ExporterTypePrometheus, + _ExporterTypeName[40:44]: ExporterTypeAuto, + _ExporterTypeLowerName[40:44]: ExporterTypeAuto, } var _ExporterTypeNames = []string{ @@ -64,6 +67,7 @@ var _ExporterTypeNames = []string{ _ExporterTypeName[20:24], _ExporterTypeName[24:30], _ExporterTypeName[30:40], + _ExporterTypeName[40:44], } // ExporterTypeString retrieves an enum value from the enum constants string name. diff --git a/backend/v3/instrumentation/logger.go b/backend/v3/instrumentation/logger.go index ac0b5b3b7..a5bc001f5 100644 --- a/backend/v3/instrumentation/logger.go +++ b/backend/v3/instrumentation/logger.go @@ -5,6 +5,7 @@ import ( "fmt" "os" + "go.opentelemetry.io/contrib/exporters/autoexport" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" "go.opentelemetry.io/otel/exporters/stdout/stdoutlog" @@ -15,6 +16,30 @@ import ( func newLoggerProvider(ctx context.Context, cfg ExporterConfig, resource *resource.Resource) (_ *sdk_log.LoggerProvider, err error) { var exporter sdk_log.Exporter switch cfg.Type { + case ExporterTypeAuto: + // autoexport delegates exporter selection to the standard OTEL env vars. + // We can't just call autoexport.NewLogExporter unconditionally because + // autoexport defaults to "otlp" when OTEL_LOGS_EXPORTER is unset, and + // the OTLP exporter silently points at localhost:4318 even with no env + // vars configured. That would cause every ZITADEL instance to start + // attempting OTLP connections after upgrading, spamming logs with + // connection errors. + // + // This guard is a heuristic: we check the env vars that realistically + // indicate someone has intentionally configured OTEL export. It does not + // cover every possible OTLP env var (e.g. OTEL_EXPORTER_OTLP_HEADERS or + // OTEL_EXPORTER_OTLP_CERTIFICATE on their own), but those vars are + // meaningless without an endpoint or exporter type being set too. + if os.Getenv("OTEL_LOGS_EXPORTER") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL") != "" { + exporter, err = autoexport.NewLogExporter(ctx) + if err == nil && autoexport.IsNoneLogExporter(exporter) { + exporter = nil + } + } case ExporterTypeUnspecified, ExporterTypeNone: // no exporter case ExporterTypeStdOut, ExporterTypeStdErr: diff --git a/backend/v3/instrumentation/logger_test.go b/backend/v3/instrumentation/logger_test.go index dfa6d9464..352eb136d 100644 --- a/backend/v3/instrumentation/logger_test.go +++ b/backend/v3/instrumentation/logger_test.go @@ -79,3 +79,140 @@ func Test_newLoggerProvider(t *testing.T) { }) } } + +func Test_newLoggerProvider_autoexport(t *testing.T) { + resource, err := resource.New(t.Context()) + require.NoError(t, err) + + tests := []struct { + name string + cfg ExporterConfig + envVars map[string]string + wantErr bool + }{ + { + name: "auto type with OTEL_LOGS_EXPORTER=console creates provider", + cfg: ExporterConfig{ + Type: ExporterTypeAuto, + }, + envVars: map[string]string{ + "OTEL_LOGS_EXPORTER": "console", + }, + }, + { + name: "auto type with no OTEL env vars creates provider (noop fallback)", + cfg: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + { + name: "auto type with OTEL_LOGS_EXPORTER=none creates provider (autoexport none)", + cfg: ExporterConfig{ + Type: ExporterTypeAuto, + }, + envVars: map[string]string{ + "OTEL_LOGS_EXPORTER": "none", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_ENDPOINT uses OTLP default", + cfg: ExporterConfig{ + Type: ExporterTypeAuto, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_LOGS_ENDPOINT uses OTLP default", + cfg: ExporterConfig{ + Type: ExporterTypeAuto, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT": "http://localhost:4318", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_PROTOCOL uses OTLP default", + cfg: ExporterConfig{ + Type: ExporterTypeAuto, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_LOGS_PROTOCOL uses OTLP default", + cfg: ExporterConfig{ + Type: ExporterTypeAuto, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": "http/protobuf", + }, + }, + { + name: "none type ignores OTEL env vars (explicit disable)", + cfg: ExporterConfig{ + Type: ExporterTypeNone, + }, + envVars: map[string]string{ + "OTEL_LOGS_EXPORTER": "invalid_exporter", + }, + }, + // Backward compatibility: ExporterTypeNone ignores global OTEL endpoint + { + name: "none type ignores OTEL_EXPORTER_OTLP_ENDPOINT", + cfg: ExporterConfig{ + Type: ExporterTypeNone, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318", + }, + }, + // Backward compatibility: ExporterTypeNone ignores per-signal OTEL var + { + name: "none type ignores OTEL_LOGS_EXPORTER=console", + cfg: ExporterConfig{ + Type: ExporterTypeNone, + }, + envVars: map[string]string{ + "OTEL_LOGS_EXPORTER": "console", + }, + }, + // Backward compatibility: explicit ZITADEL types take priority over OTEL env vars + { + name: "stdout type ignores OTEL_LOGS_EXPORTER", + cfg: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + envVars: map[string]string{ + "OTEL_LOGS_EXPORTER": "grpc", + }, + }, + { + name: "grpc type ignores OTEL_EXPORTER_OTLP_ENDPOINT", + cfg: ExporterConfig{ + Type: ExporterTypeGRPC, + Endpoint: "localhost:4317", + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://some-other-host:4318", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for k, v := range tt.envVars { + t.Setenv(k, v) + } + got, gotErr := newLoggerProvider(t.Context(), tt.cfg, resource) + if tt.wantErr { + assert.Error(t, gotErr) + assert.Nil(t, got) + } else { + assert.NoError(t, gotErr) + assert.NotNil(t, got) + } + }) + } +} diff --git a/backend/v3/instrumentation/metric.go b/backend/v3/instrumentation/metric.go index 01c0d05bb..2221cdfc3 100644 --- a/backend/v3/instrumentation/metric.go +++ b/backend/v3/instrumentation/metric.go @@ -9,6 +9,7 @@ import ( google_metric "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric" "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/contrib/exporters/autoexport" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" @@ -160,6 +161,31 @@ func labelsToAttributes(labels map[string]attribute.Value) []attribute.KeyValue func newMeterProvider(ctx context.Context, cfg MetricConfig, resource *resource.Resource) (_ *sdk_metric.MeterProvider, err error) { var readerOption sdk_metric.Option switch cfg.Exporter.Type { + case ExporterTypeAuto: + // autoexport delegates reader selection to the standard OTEL env vars. + // We can't just call autoexport.NewMetricReader unconditionally because + // autoexport defaults to "otlp" when OTEL_METRICS_EXPORTER is unset, and + // the OTLP exporter silently points at localhost:4318 even with no env + // vars configured. That would cause every ZITADEL instance to start + // attempting OTLP connections after upgrading, spamming logs with + // connection errors. + // + // This guard is a heuristic: we check the env vars that realistically + // indicate someone has intentionally configured OTEL export. It does not + // cover every possible OTLP env var (e.g. OTEL_EXPORTER_OTLP_HEADERS or + // OTEL_EXPORTER_OTLP_CERTIFICATE on their own), but those vars are + // meaningless without an endpoint or exporter type being set too. + if os.Getenv("OTEL_METRICS_EXPORTER") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_METRICS_PROTOCOL") != "" { + var reader sdk_metric.Reader + reader, err = autoexport.NewMetricReader(ctx) + if err == nil && !autoexport.IsNoneMetricReader(reader) { + readerOption = sdk_metric.WithReader(reader) + } + } case ExporterTypeUnspecified, ExporterTypeNone: // no reader option case ExporterTypeStdOut, ExporterTypeStdErr: diff --git a/backend/v3/instrumentation/metric_test.go b/backend/v3/instrumentation/metric_test.go index c2ffef1a8..46cf3d7e8 100644 --- a/backend/v3/instrumentation/metric_test.go +++ b/backend/v3/instrumentation/metric_test.go @@ -77,6 +77,22 @@ func TestMetricConfig_SetLegacyConfig(t *testing.T) { }, }, }, + { + name: "auto type is not overridden by legacy config", + target: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + lc: &LegacyMetricConfig{ + Type: "otel", + }, + want: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -184,3 +200,164 @@ func Test_newMeterProvider(t *testing.T) { }) } } + +func Test_newMeterProvider_autoexport(t *testing.T) { + resource, err := resource.New(t.Context()) + require.NoError(t, err) + + tests := []struct { + name string + cfg MetricConfig + envVars map[string]string + wantErr bool + }{ + { + name: "auto type with OTEL_METRICS_EXPORTER=console creates provider", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_METRICS_EXPORTER": "console", + }, + }, + { + name: "auto type with no OTEL env vars creates provider (noop fallback)", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + }, + { + name: "auto type with OTEL_METRICS_EXPORTER=none creates provider (autoexport none)", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_METRICS_EXPORTER": "none", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_ENDPOINT uses OTLP default", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_METRICS_ENDPOINT uses OTLP default", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT": "http://localhost:4318", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_PROTOCOL uses OTLP default", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_METRICS_PROTOCOL uses OTLP default", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "http/protobuf", + }, + }, + { + name: "none type ignores OTEL env vars (explicit disable)", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + envVars: map[string]string{ + "OTEL_METRICS_EXPORTER": "invalid_exporter", + }, + }, + // Backward compatibility: ExporterTypeNone ignores global OTEL endpoint + { + name: "none type ignores OTEL_EXPORTER_OTLP_ENDPOINT", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318", + }, + }, + // Backward compatibility: ExporterTypeNone ignores per-signal OTEL var + { + name: "none type ignores OTEL_METRICS_EXPORTER=console", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + envVars: map[string]string{ + "OTEL_METRICS_EXPORTER": "console", + }, + }, + // Backward compatibility: explicit ZITADEL types take priority over OTEL env vars + { + name: "stdout type ignores OTEL_METRICS_EXPORTER", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + envVars: map[string]string{ + "OTEL_METRICS_EXPORTER": "grpc", + }, + }, + { + name: "grpc type ignores OTEL_EXPORTER_OTLP_ENDPOINT", + cfg: MetricConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeGRPC, + Endpoint: "localhost:4317", + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://some-other-host:4318", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for k, v := range tt.envVars { + t.Setenv(k, v) + } + got, gotErr := newMeterProvider(t.Context(), tt.cfg, resource) + if tt.wantErr { + assert.Error(t, gotErr) + assert.Nil(t, got) + } else { + assert.NoError(t, gotErr) + assert.NotNil(t, got) + } + }) + } +} diff --git a/backend/v3/instrumentation/otel.go b/backend/v3/instrumentation/otel.go index 32ae63c91..9e85ba63a 100644 --- a/backend/v3/instrumentation/otel.go +++ b/backend/v3/instrumentation/otel.go @@ -36,6 +36,7 @@ const ( ExporterTypeHTTP ExporterTypeGoogle ExporterTypePrometheus + ExporterTypeAuto ) // isNone returns true if the ExporterType is either Unspecified or None. diff --git a/backend/v3/instrumentation/tracing.go b/backend/v3/instrumentation/tracing.go index 57426c411..89776f9b9 100644 --- a/backend/v3/instrumentation/tracing.go +++ b/backend/v3/instrumentation/tracing.go @@ -9,6 +9,7 @@ import ( "slices" google_trace "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace" + "go.opentelemetry.io/contrib/exporters/autoexport" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" @@ -136,6 +137,30 @@ func (s *Span) SetStatusByError(err error) { func newTracerProvider(ctx context.Context, cfg TraceConfig, resource *resource.Resource) (_ *sdk_trace.TracerProvider, err error) { var exporter sdk_trace.SpanExporter switch cfg.Exporter.Type { + case ExporterTypeAuto: + // autoexport delegates exporter selection to the standard OTEL env vars. + // We can't just call autoexport.NewSpanExporter unconditionally because + // autoexport defaults to "otlp" when OTEL_TRACES_EXPORTER is unset, and + // the OTLP exporter silently points at localhost:4318 even with no env + // vars configured. That would cause every ZITADEL instance to start + // attempting OTLP connections after upgrading, spamming logs with + // connection errors. + // + // This guard is a heuristic: we check the env vars that realistically + // indicate someone has intentionally configured OTEL export. It does not + // cover every possible OTLP env var (e.g. OTEL_EXPORTER_OTLP_HEADERS or + // OTEL_EXPORTER_OTLP_CERTIFICATE on their own), but those vars are + // meaningless without an endpoint or exporter type being set too. + if os.Getenv("OTEL_TRACES_EXPORTER") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL") != "" || + os.Getenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL") != "" { + exporter, err = autoexport.NewSpanExporter(ctx) + if err == nil && autoexport.IsNoneSpanExporter(exporter) { + exporter = nil + } + } case ExporterTypeUnspecified, ExporterTypeNone: // no exporter case ExporterTypeStdOut, ExporterTypeStdErr: @@ -147,7 +172,7 @@ func newTracerProvider(ctx context.Context, cfg TraceConfig, resource *resource. case ExporterTypeGoogle: exporter, err = traceGoogleExporter(ctx, cfg.Exporter) case ExporterTypePrometheus: - fallthrough // prometheus is not supported for logs + fallthrough // prometheus is not supported for traces default: err = errExporterType(cfg.Exporter.Type, "tracer") } @@ -160,7 +185,7 @@ func newTracerProvider(ctx context.Context, cfg TraceConfig, resource *resource. sdk_trace.WithSampler(sampler), } if exporter != nil { - opts = append(opts, sdk_trace.WithBatcher(exporter)) + opts = append(opts, sdk_trace.WithBatcher(exporter, sdk_trace.WithBatchTimeout(cfg.Exporter.BatchDuration))) } tracerProvider := sdk_trace.NewTracerProvider(opts...) return tracerProvider, nil diff --git a/backend/v3/instrumentation/tracing_test.go b/backend/v3/instrumentation/tracing_test.go index 54c5cfbd6..c8fddd642 100644 --- a/backend/v3/instrumentation/tracing_test.go +++ b/backend/v3/instrumentation/tracing_test.go @@ -87,6 +87,24 @@ func TestTraceConfig_SetLegacyConfig(t *testing.T) { }, }, }, + { + name: "auto type is not overridden by legacy config", + target: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + lc: &LegacyTraceConfig{ + Type: "google", + Fraction: 0.5, + Endpoint: "endpoint", + }, + want: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -189,3 +207,164 @@ func Test_newTracerProvider(t *testing.T) { }) } } + +func Test_newTracerProvider_autoexport(t *testing.T) { + resource, err := resource.New(t.Context()) + require.NoError(t, err) + + tests := []struct { + name string + cfg TraceConfig + envVars map[string]string + wantErr bool + }{ + { + name: "auto type with OTEL_TRACES_EXPORTER=console creates provider", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_TRACES_EXPORTER": "console", + }, + }, + { + name: "auto type with no OTEL env vars creates provider (noop fallback)", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + }, + { + name: "auto type with OTEL_TRACES_EXPORTER=none creates provider (autoexport none)", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_TRACES_EXPORTER": "none", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_ENDPOINT uses OTLP default", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_TRACES_ENDPOINT uses OTLP default", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT": "http://localhost:4318", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_PROTOCOL uses OTLP default", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", + }, + }, + { + name: "auto type with only OTEL_EXPORTER_OTLP_TRACES_PROTOCOL uses OTLP default", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeAuto, + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": "http/protobuf", + }, + }, + { + name: "none type ignores OTEL env vars (explicit disable)", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + envVars: map[string]string{ + "OTEL_TRACES_EXPORTER": "invalid_exporter", + }, + }, + // Backward compatibility: ExporterTypeNone ignores global OTEL endpoint + { + name: "none type ignores OTEL_EXPORTER_OTLP_ENDPOINT", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318", + }, + }, + // Backward compatibility: ExporterTypeNone ignores per-signal OTEL var + { + name: "none type ignores OTEL_TRACES_EXPORTER=console", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeNone, + }, + }, + envVars: map[string]string{ + "OTEL_TRACES_EXPORTER": "console", + }, + }, + // Backward compatibility: explicit ZITADEL types take priority over OTEL env vars + { + name: "stdout type ignores OTEL_TRACES_EXPORTER", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeStdOut, + }, + }, + envVars: map[string]string{ + "OTEL_TRACES_EXPORTER": "grpc", + }, + }, + { + name: "grpc type ignores OTEL_EXPORTER_OTLP_ENDPOINT", + cfg: TraceConfig{ + Exporter: ExporterConfig{ + Type: ExporterTypeGRPC, + Endpoint: "localhost:4317", + }, + }, + envVars: map[string]string{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://some-other-host:4318", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for k, v := range tt.envVars { + t.Setenv(k, v) + } + got, gotErr := newTracerProvider(t.Context(), tt.cfg, resource) + if tt.wantErr { + assert.Error(t, gotErr) + assert.Nil(t, got) + } else { + assert.NoError(t, gotErr) + assert.NotNil(t, got) + } + }) + } +} diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index de89d7f45..72e266b27 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -8,6 +8,7 @@ Instrumentation: Exporter: # The following exporter types are supported: # "none": Disables trace exporting + # "auto": Delegates to standard OTEL env vars (OTEL_TRACES_EXPORTER, OTEL_EXPORTER_OTLP_ENDPOINT) # "stdOut": Exports traces to standard output # "stdErr": Exports traces to standard error # "grpc": Exports traces using the OTEL gRPC exporter (recommended) @@ -26,6 +27,7 @@ Instrumentation: Exporter: # The following exporter types are supported: # "none": Disables metric exporting + # "auto": Delegates to standard OTEL env vars (OTEL_METRICS_EXPORTER, OTEL_EXPORTER_OTLP_ENDPOINT) # "stdOut": Exports metrics to standard output # "stdErr": Exports metrics to standard error # "grpc": Exports metrics using the OTEL gRPC exporter (recommended) @@ -91,6 +93,7 @@ Instrumentation: Exporter: # The following exporter types are supported: # - "none": Disables OTEL log exporter + # - "auto": Delegates to standard OTEL env vars (OTEL_LOGS_EXPORTER, OTEL_EXPORTER_OTLP_ENDPOINT) # - "stdOut": Exports OTEL logs to standard output # - "stdErr": Exports OTEL logs to standard error # - "grpc": Exports OTEL logs using the OTEL gRPC exporter (recommended) @@ -100,7 +103,7 @@ Instrumentation: Endpoint: "" # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_ENDPOINT # Disable TLS for grpc and http exporters Insecure: false # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_INSECURE - # Interval at which metrics are exported + # Interval at which logs are batched for export BatchDuration: 1s # ZITADEL_INSTRUMENTATION_LOG_EXPORTER_BATCHDURATION Profile: Exporter: diff --git a/go.mod b/go.mod index 40eb50157..a576ff1fe 100644 --- a/go.mod +++ b/go.mod @@ -106,6 +106,7 @@ require ( github.com/zitadel/schema v1.3.2 github.com/zitadel/sloggcp v0.6.0 go.opentelemetry.io/contrib/bridges/otelslog v0.17.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 go.opentelemetry.io/otel v1.42.0 @@ -257,6 +258,7 @@ require ( github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect diff --git a/go.sum b/go.sum index e3de03185..ca9ae2d27 100644 --- a/go.sum +++ b/go.sum @@ -856,8 +856,12 @@ 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/bridges/otelslog v0.17.0 h1:NFIS6x7wyObQ7cR84x7bt1sr8nYBx89s3x3GwRjw40k= go.opentelemetry.io/contrib/bridges/otelslog v0.17.0/go.mod h1:39SaByOyDMRMe872AE7uelMuQZidIw7LLFAnQi0FWTE= +go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk= +go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4= go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= +go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8= +go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=