Files
telemetry/telemetry.go
Rene Nochebuena ed4e9ef161 feat(telemetry): initial stable release v0.9.0
Single-call OTel SDK bootstrap setting all three global providers (traces → Tempo, metrics → Mimir, logs → Loki) over OTLP gRPC.

What's included:
- New(ctx, Config): bootstraps TracerProvider, MeterProvider, and LoggerProvider with OTLP gRPC exporters; sets OTel globals
- W3C TraceContext + Baggage propagation set globally
- Resource tagging: service.name, service.version, deployment.environment merged with SDK defaults
- OTLPInsecure bool for development environments without TLS
- Sequential rollback on partial initialization failure — no dangling exporters on error
- Returns shutdown func(context.Context) error; caller defers in main or wires into launcher BeforeStop
- Tier 5 module: must be imported only by application main packages; zero micro-lib dependencies

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
2026-03-18 14:13:29 -06:00

130 lines
4.2 KiB
Go

package telemetry
import (
"context"
"errors"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
otlploggrpc "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
otlpmetricgrpc "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
otlptracegrpc "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/log/global"
"go.opentelemetry.io/otel/propagation"
sdklog "go.opentelemetry.io/otel/sdk/log"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)
// Config holds OTel bootstrap configuration.
type Config struct {
// ServiceName identifies the service in traces, metrics, and logs.
ServiceName string `env:"OTEL_SERVICE_NAME,required"`
// ServiceVersion is the deployed version (e.g. "1.4.2").
ServiceVersion string `env:"OTEL_SERVICE_VERSION" envDefault:"unknown"`
// Environment is the deployment environment (e.g. "production", "staging").
Environment string `env:"OTEL_ENVIRONMENT" envDefault:"development"`
// OTLPEndpoint is the OTLP gRPC collector address (e.g. "alloy:4317").
OTLPEndpoint string `env:"OTEL_EXPORTER_OTLP_ENDPOINT,required"`
// OTLPInsecure disables TLS for the OTLP connection. Set true in development.
OTLPInsecure bool `env:"OTEL_EXPORTER_OTLP_INSECURE" envDefault:"false"`
}
// New bootstraps the full OTel SDK:
// - TracerProvider → OTLP gRPC → Grafana Alloy → Tempo
// - MeterProvider → OTLP gRPC → Grafana Alloy → Mimir
// - LoggerProvider → OTLP gRPC → Grafana Alloy → Loki
//
// Sets the three OTel globals so all micro-libs using the global API
// auto-instrument without importing this module.
//
// The returned shutdown function flushes all exporters and must be called
// before process exit (defer it in main or wire it into the launcher).
// Returns (shutdown, nil) on success, (nil, err) on failure.
func New(ctx context.Context, cfg Config) (func(context.Context) error, error) {
res := newResource(cfg)
// --- TracerProvider (traces → Tempo) ---
traceOpts := []otlptracegrpc.Option{
otlptracegrpc.WithEndpoint(cfg.OTLPEndpoint),
}
if cfg.OTLPInsecure {
traceOpts = append(traceOpts, otlptracegrpc.WithInsecure())
}
traceExporter, err := otlptracegrpc.New(ctx, traceOpts...)
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(traceExporter),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
// --- MeterProvider (metrics → Mimir) ---
metricOpts := []otlpmetricgrpc.Option{
otlpmetricgrpc.WithEndpoint(cfg.OTLPEndpoint),
}
if cfg.OTLPInsecure {
metricOpts = append(metricOpts, otlpmetricgrpc.WithInsecure())
}
metricExporter, err := otlpmetricgrpc.New(ctx, metricOpts...)
if err != nil {
_ = tp.Shutdown(ctx)
return nil, err
}
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter)),
sdkmetric.WithResource(res),
)
otel.SetMeterProvider(mp)
// --- LoggerProvider (logs → Loki) ---
logOpts := []otlploggrpc.Option{
otlploggrpc.WithEndpoint(cfg.OTLPEndpoint),
}
if cfg.OTLPInsecure {
logOpts = append(logOpts, otlploggrpc.WithInsecure())
}
logExporter, err := otlploggrpc.New(ctx, logOpts...)
if err != nil {
_ = tp.Shutdown(ctx)
_ = mp.Shutdown(ctx)
return nil, err
}
lp := sdklog.NewLoggerProvider(
sdklog.WithProcessor(sdklog.NewBatchProcessor(logExporter)),
sdklog.WithResource(res),
)
global.SetLoggerProvider(lp)
shutdown := func(ctx context.Context) error {
return errors.Join(
tp.Shutdown(ctx),
mp.Shutdown(ctx),
lp.Shutdown(ctx),
)
}
return shutdown, nil
}
// newResource builds an OTel resource with service identity and environment attributes.
func newResource(cfg Config) *resource.Resource {
r, _ := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(cfg.ServiceName),
semconv.ServiceVersion(cfg.ServiceVersion),
attribute.String("deployment.environment", cfg.Environment),
),
)
return r
}