fix(otel): keep telemetry off unless an exporter is configured

This commit is contained in:
Julien Neuhart
2026-08-31 13:26:58 +02:00
parent 0b16b0ab34
commit c636a52666
2 changed files with 86 additions and 21 deletions

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"log/slog"
"os"
"strings"
"sync/atomic"
"go.opentelemetry.io/contrib/bridges/otelslog"
@@ -74,6 +75,24 @@ func buildResource(ctx context.Context, logger *slog.Logger, serviceName, servic
return merged
}
// OTEL_*_EXPORTER select the exporter for each signal. autoexport treats an
// unset or empty value as a request for the OTLP exporter, which then fails
// against the default localhost:4318 endpoint when nothing listens there and,
// for metrics, keeps retrying on the periodic reader's timer. Gotenberg keeps
// telemetry opt-in: a signal with no exporter configured is built without one
// and stays inert. See https://github.com/gotenberg/gotenberg/issues/1643.
const (
tracesExporterEnvKey = "OTEL_TRACES_EXPORTER"
metricsExporterEnvKey = "OTEL_METRICS_EXPORTER"
logsExporterEnvKey = "OTEL_LOGS_EXPORTER"
)
// exporterConfigured reports whether the operator selected an exporter for the
// signal owning envKey. An unset or blank value keeps that signal off.
func exporterConfigured(envKey string) bool {
return strings.TrimSpace(os.Getenv(envKey)) != ""
}
// InitTracerProvider initializes the OpenTelemetry tracer provider.
func InitTracerProvider(logger *slog.Logger, serviceName, serviceVersion string) (shutdown func(context.Context) error, err error) {
initOtelLogger(logger)
@@ -86,13 +105,14 @@ func InitTracerProvider(logger *slog.Logger, serviceName, serviceVersion string)
trace.WithResource(res),
}
traceExporter, err := autoexport.NewSpanExporter(ctx)
if err != nil {
return nil, err
}
if !autoexport.IsNoneSpanExporter(traceExporter) {
traceOpts = append(traceOpts, trace.WithBatcher(traceExporter))
if exporterConfigured(tracesExporterEnvKey) {
traceExporter, err := autoexport.NewSpanExporter(ctx)
if err != nil {
return nil, err
}
if !autoexport.IsNoneSpanExporter(traceExporter) {
traceOpts = append(traceOpts, trace.WithBatcher(traceExporter))
}
}
traceProvider := trace.NewTracerProvider(traceOpts...)
@@ -119,13 +139,14 @@ func InitMeterProvider(logger *slog.Logger, serviceName, serviceVersion string)
}
metricOpts = append(metricOpts, exemplarFilterOptions()...)
metricReader, err := autoexport.NewMetricReader(ctx)
if err != nil {
return nil, err
}
if !autoexport.IsNoneMetricReader(metricReader) {
metricOpts = append(metricOpts, metric.WithReader(metricReader))
if exporterConfigured(metricsExporterEnvKey) {
metricReader, err := autoexport.NewMetricReader(ctx)
if err != nil {
return nil, err
}
if !autoexport.IsNoneMetricReader(metricReader) {
metricOpts = append(metricOpts, metric.WithReader(metricReader))
}
}
meterProvider := metric.NewMeterProvider(metricOpts...)
@@ -157,13 +178,14 @@ func InitLoggerProvider(logger *slog.Logger, serviceName, serviceVersion string)
log.WithResource(res),
}
logExporter, err := autoexport.NewLogExporter(ctx)
if err != nil {
return nil, nil, err
}
if !autoexport.IsNoneLogExporter(logExporter) {
logOpts = append(logOpts, log.WithProcessor(log.NewBatchProcessor(logExporter)))
if exporterConfigured(logsExporterEnvKey) {
logExporter, err := autoexport.NewLogExporter(ctx)
if err != nil {
return nil, nil, err
}
if !autoexport.IsNoneLogExporter(logExporter) {
logOpts = append(logOpts, log.WithProcessor(log.NewBatchProcessor(logExporter)))
}
}
loggerProvider := log.NewLoggerProvider(logOpts...)

View File

@@ -89,6 +89,49 @@ func TestInitTracerProvider_HonorsSamplerEnv(t *testing.T) {
}
}
// TestExporterConfigured pins the opt-in gate: an unset or blank
// OTEL_*_EXPORTER keeps the signal off, so Gotenberg never wires the OTLP
// exporter that autoexport would otherwise default to and fail to reach at
// localhost:4318. See https://github.com/gotenberg/gotenberg/issues/1643.
func TestExporterConfigured(t *testing.T) {
const key = "OTEL_METRICS_EXPORTER"
orig, had := os.LookupEnv(key)
t.Cleanup(func() {
if had {
os.Setenv(key, orig)
return
}
os.Unsetenv(key)
})
for _, tc := range []struct {
scenario string
unset bool
value string
want bool
}{
{"unset", true, "", false},
{"empty", false, "", false},
{"whitespace only", false, " ", false},
{"none", false, "none", true},
{"otlp", false, "otlp", true},
{"padded value", false, " otlp ", true},
} {
t.Run(tc.scenario, func(t *testing.T) {
if tc.unset {
os.Unsetenv(key)
} else {
os.Setenv(key, tc.value)
}
if got := exporterConfigured(key); got != tc.want {
t.Errorf("exporterConfigured(%q) = %v, want %v", key, got, tc.want)
}
})
}
}
func TestExemplarFilterOptions(t *testing.T) {
t.Run("default pins trace-based", func(t *testing.T) {
if v, ok := os.LookupEnv("OTEL_METRICS_EXEMPLAR_FILTER"); ok {