85 lines
2.9 KiB
Go
85 lines
2.9 KiB
Go
// 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) }
|