mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-10-14 00:03:18 +01:00
fix(otel): keep telemetry off unless an exporter is configured
This commit is contained in:
@@ -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...)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user