// Package telemetry holds shared OpenTelemetry setup helpers used by the // service main packages (cart, checkout, profile, …). package telemetry import ( "context" "math/rand/v2" "os" "strconv" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/sdk/log" ) // LoggerConfig controls the optional OTLP log pipeline. // The zero value disables logging (Enabled = false, SampleRatio = 0). type LoggerConfig struct { Enabled bool SampleRatio float64 // 0.0–1.0, clamped by NewLoggerProvider } // LoggerConfigFromEnv reads OTEL_LOGS_ENABLED and OTEL_LOGS_SAMPLE_RATIO from // the environment. Call this from main(); library consumers that want explicit // control should construct LoggerConfig directly. func LoggerConfigFromEnv() LoggerConfig { var enabled bool switch os.Getenv("OTEL_LOGS_ENABLED") { case "1", "true", "TRUE", "True", "yes", "on": enabled = true } ratio := 1.0 if v := os.Getenv("OTEL_LOGS_SAMPLE_RATIO"); v != "" { if r, err := strconv.ParseFloat(v, 64); err == nil && r >= 0 && r <= 1 { ratio = r } } return LoggerConfig{Enabled: enabled, SampleRatio: ratio} } // NewLoggerProvider builds the OTLP log provider from cfg, or returns // (nil, nil) when cfg.Enabled is false — in which case the caller MUST skip // global.SetLoggerProvider and the shutdown registration. // // Use LoggerConfigFromEnv() to read the conventional OTEL_LOGS_ENABLED / // OTEL_LOGS_SAMPLE_RATIO env vars, or construct LoggerConfig directly for // explicit control (e.g. tests). Default OFF: the batch processor clones its // record ring on every export, which profiling showed to be the dominant heap // allocator in these services. Traces and metrics are always on regardless. func NewLoggerProvider(ctx context.Context, cfg LoggerConfig) (*log.LoggerProvider, error) { if !cfg.Enabled { return nil, nil } exporter, err := otlploggrpc.New(ctx) if err != nil { return nil, err } var proc log.Processor = log.NewBatchProcessor(exporter) if cfg.SampleRatio < 1.0 { proc = &samplingProcessor{next: proc, ratio: cfg.SampleRatio} } return log.NewLoggerProvider(log.WithProcessor(proc)), nil } // samplingProcessor forwards a random `ratio` fraction of records to the wrapped // processor and drops the rest before they reach the (allocation-heavy) batch // ring. This is head sampling: cheap, stateless, and per-record independent. type samplingProcessor struct { next log.Processor ratio float64 } func (s *samplingProcessor) Enabled(ctx context.Context, param log.EnabledParameters) bool { return s.next.Enabled(ctx, param) } func (s *samplingProcessor) OnEmit(ctx context.Context, record *log.Record) error { if rand.Float64() >= s.ratio { return nil // dropped by sampling } return s.next.OnEmit(ctx, record) } func (s *samplingProcessor) Shutdown(ctx context.Context) error { return s.next.Shutdown(ctx) } func (s *samplingProcessor) ForceFlush(ctx context.Context) error { return s.next.ForceFlush(ctx) }