169 lines
6.4 KiB
Go
169 lines
6.4 KiB
Go
// 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
|
||
|
|
}
|
||
|
|
}
|