Files

169 lines
6.4 KiB
Go
Raw Permalink Normal View History

// Package telemetry provides the OpenTelemetry SDK bootstrap for Silo: shared
// resource, tracer provider, logger provider, and W3C propagation. It
// deliberately does NOT build or register a MeterProvider — metrics stay on the
// existing Prometheus rail. Leaving the global MeterProvider as the built-in
// no-op is what prevents the trace instrumentation libraries from double-emitting
// metrics. See docs/superpowers/plans/2026-07-02-opentelemetry-observability.md.
package telemetry
import (
"math"
"os"
"strconv"
"strings"
)
// Protocol identifies the OTLP exporter wire protocol.
type Protocol string
const (
// ProtocolGRPC is the OTLP/gRPC protocol (default).
ProtocolGRPC Protocol = "grpc"
// ProtocolHTTP is the OTLP/HTTP+protobuf protocol.
ProtocolHTTP Protocol = "http/protobuf"
)
// Sampler identifies the head-sampling strategy (OTEL_TRACES_SAMPLER).
type Sampler string
const (
// SamplerAlwaysOn samples every trace.
SamplerAlwaysOn Sampler = "always_on"
// SamplerAlwaysOff samples nothing.
SamplerAlwaysOff Sampler = "always_off"
// SamplerTraceIDRatio samples by trace-id ratio regardless of the parent.
SamplerTraceIDRatio Sampler = "traceidratio"
// SamplerParentBasedAlwaysOn honors the parent decision, sampling roots.
SamplerParentBasedAlwaysOn Sampler = "parentbased_always_on"
// SamplerParentBasedAlwaysOff honors the parent decision, dropping roots.
SamplerParentBasedAlwaysOff Sampler = "parentbased_always_off"
// SamplerParentBasedTraceIDRatio honors the parent decision, sampling roots
// by trace-id ratio. This is the default.
SamplerParentBasedTraceIDRatio Sampler = "parentbased_traceidratio"
)
// defaultServiceName is used when OTEL_SERVICE_NAME is unset.
const defaultServiceName = "silo-server"
// defaultSamplerRatio is the parent-based trace-id ratio applied when
// OTEL_TRACES_SAMPLER_ARG is unset or unparseable.
const defaultSamplerRatio = 1.0
// Config is the fully-defaulted telemetry configuration parsed from the
// environment. It is cheap to construct and safe to build even when telemetry
// is disabled.
type Config struct {
// Enabled gates the entire feature. True when SILO_OTEL_ENABLED is truthy
// OR OTEL_EXPORTER_OTLP_ENDPOINT is set.
Enabled bool
// Endpoint is the OTLP collector endpoint (OTEL_EXPORTER_OTLP_ENDPOINT). It
// is used ONLY to decide Enabled; the exporters themselves read the endpoint
// (and all other OTEL_EXPORTER_OTLP_* knobs) directly from the environment,
// which remains the single source of truth for exporter wiring.
Endpoint string
// Protocol is the generic OTLP wire protocol (OTEL_EXPORTER_OTLP_PROTOCOL).
// It is the fallback for any signal without a signal-specific override.
Protocol Protocol
// TracesProtocol selects the trace exporter's wire protocol, honoring
// OTEL_EXPORTER_OTLP_TRACES_PROTOCOL and falling back to Protocol.
TracesProtocol Protocol
// LogsProtocol selects the log exporter's wire protocol, honoring
// OTEL_EXPORTER_OTLP_LOGS_PROTOCOL and falling back to Protocol.
LogsProtocol Protocol
// ServiceName populates the service.name resource attribute.
ServiceName string
// ServiceVersion populates the service.version resource attribute.
ServiceVersion string
// NodeID populates the service.instance.id resource attribute.
NodeID string
// Sampler is the head-sampling strategy (OTEL_TRACES_SAMPLER). Unrecognized
// or unsupported values (e.g. jaeger_remote) fall back to
// parentbased_traceidratio.
Sampler Sampler
// SamplerRatio is the trace-id-ratio sampling probability used by the
// ratio-based samplers (OTEL_TRACES_SAMPLER_ARG).
SamplerRatio float64
}
// LoadConfig parses the telemetry configuration from the environment. nodeID is
// the resolved node identity used for the service.instance.id resource
// attribute.
func LoadConfig(nodeID string) Config {
endpoint := strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"))
enabled := truthy(os.Getenv("SILO_OTEL_ENABLED")) || endpoint != ""
serviceName := strings.TrimSpace(os.Getenv("OTEL_SERVICE_NAME"))
if serviceName == "" {
serviceName = defaultServiceName
}
// The generic protocol is the fallback; per-signal env vars override it for
// their own exporter so mixed collector setups (e.g. HTTP logs, gRPC traces)
// work as the OTLP spec prescribes.
protocol := parseProtocol(os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL"), ProtocolGRPC)
tracesProtocol := parseProtocol(os.Getenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"), protocol)
logsProtocol := parseProtocol(os.Getenv("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"), protocol)
ratio := defaultSamplerRatio
if raw := strings.TrimSpace(os.Getenv("OTEL_TRACES_SAMPLER_ARG")); raw != "" {
// Accept only finite, non-negative values; clamp above 1 to 1.0 so a
// typo'd or +Inf arg means "sample everything" rather than silently
// falling through. NaN and -Inf fail the v >= 0 / IsInf checks.
if v, err := strconv.ParseFloat(raw, 64); err == nil && v >= 0 && !math.IsInf(v, 1) {
ratio = math.Min(v, 1.0)
}
}
return Config{
Enabled: enabled,
Endpoint: endpoint,
Protocol: protocol,
TracesProtocol: tracesProtocol,
LogsProtocol: logsProtocol,
ServiceName: serviceName,
ServiceVersion: strings.TrimSpace(os.Getenv("OTEL_SERVICE_VERSION")),
NodeID: nodeID,
Sampler: parseSampler(os.Getenv("OTEL_TRACES_SAMPLER")),
SamplerRatio: ratio,
}
}
// parseSampler maps an OTEL_TRACES_SAMPLER value to a Sampler, falling back to
// parentbased_traceidratio when the value is empty, unrecognized, or names a
// sampler this bootstrap does not support (e.g. jaeger_remote).
func parseSampler(raw string) Sampler {
switch s := Sampler(strings.ToLower(strings.TrimSpace(raw))); s {
case SamplerAlwaysOn, SamplerAlwaysOff, SamplerTraceIDRatio,
SamplerParentBasedAlwaysOn, SamplerParentBasedAlwaysOff:
return s
default:
return SamplerParentBasedTraceIDRatio
}
}
// parseProtocol maps an OTEL_EXPORTER_OTLP*_PROTOCOL value to a Protocol,
// returning fallback when the value is empty or unrecognized.
func parseProtocol(raw string, fallback Protocol) Protocol {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "http/protobuf", "http":
return ProtocolHTTP
case "grpc":
return ProtocolGRPC
default:
return fallback
}
}
// truthy reports whether an env value should be treated as a boolean true.
func truthy(v string) bool {
switch strings.ToLower(strings.TrimSpace(v)) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}