feat(telemetry): initial implementation — OpenTelemetry traces, metrics, logs (v1.0.0)
Introduces code.nochebuena.dev/einherjar/telemetry — the observability bootstrap starter for the Einherjar framework. Absorbs the telemetry package from micro-lib, migrating from OpenCensus to OpenTelemetry SDK v1.42. Bootstrap functions (not lifecycle.Component — telemetry must be initialized before the launcher starts, and its shutdown must run after all components stop): - New(ctx, cfg) (func(context.Context) error, error) — production mode; exports traces, metrics, and logs via OTLP over gRPC to the configured endpoint; returns a shutdown function to be deferred in main() - NewConsole(ctx, logger, cfg) (func(context.Context) error, error) — development mode; writes structured telemetry to the provided logging.Logger; no network dependency; suitable for local development and CI Config (EINHERJAR_OTEL_* env vars): ServiceName(required), ServiceVersion(unknown), Environment(development), OTLPEndpoint(required for New), OTLPInsecure(false) ConsoleConfig (EINHERJAR_OTEL_* env vars): ServiceName(required), ServiceVersion(unknown), Environment(development) - identifiable.go: package-level Module variable (observability.Identifiable) for version identification — telemetry bootstraps before the launcher; not registered as a lifecycle component
This commit is contained in:
176
console.go
Normal file
176
console.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
otellog "go.opentelemetry.io/otel/log"
|
||||
"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/metric/metricdata"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
|
||||
|
||||
"code.nochebuena.dev/einherjar/contracts/logging"
|
||||
)
|
||||
|
||||
// NewConsole bootstraps the OTel SDK with logger-backed exporters for local development.
|
||||
// All signals (traces, metrics, OTel log records) are emitted as structured log lines
|
||||
// instead of being sent to a collector. Drop-in alternative to [New].
|
||||
//
|
||||
// Warning: do not use the OTel slog bridge (otelslog) together with NewConsole.
|
||||
// The bridge routes slog records into the OTel log API; logLogExporter then writes
|
||||
// them back to the same logger — creating an infinite feedback loop. See package
|
||||
// documentation for the full explanation and safe usage patterns.
|
||||
func NewConsole(ctx context.Context, logger logging.Logger, cfg ConsoleConfig) (func(context.Context) error, error) {
|
||||
// Empty schema URL avoids merge conflicts with resource.Default()'s internal schema URL.
|
||||
res, err := resource.Merge(
|
||||
resource.Default(),
|
||||
resource.NewWithAttributes(
|
||||
"",
|
||||
semconv.ServiceName(cfg.ServiceName),
|
||||
semconv.ServiceVersion(cfg.ServiceVersion),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
res = resource.Default()
|
||||
}
|
||||
|
||||
tp := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSyncer(&logTraceExporter{logger: logger}),
|
||||
sdktrace.WithResource(res),
|
||||
)
|
||||
otel.SetTracerProvider(tp)
|
||||
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{},
|
||||
propagation.Baggage{},
|
||||
))
|
||||
|
||||
mp := sdkmetric.NewMeterProvider(
|
||||
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(&logMetricExporter{logger: logger})),
|
||||
sdkmetric.WithResource(res),
|
||||
)
|
||||
otel.SetMeterProvider(mp)
|
||||
|
||||
lp := sdklog.NewLoggerProvider(
|
||||
sdklog.WithProcessor(sdklog.NewBatchProcessor(&logLogExporter{logger: logger})),
|
||||
sdklog.WithResource(res),
|
||||
)
|
||||
global.SetLoggerProvider(lp)
|
||||
|
||||
shutdown := func(ctx context.Context) error {
|
||||
var errs []error
|
||||
if err := tp.Shutdown(ctx); err != nil {
|
||||
errs = append(errs, &providerErr{"trace", err})
|
||||
}
|
||||
if err := mp.Shutdown(ctx); err != nil {
|
||||
errs = append(errs, &providerErr{"metric", err})
|
||||
}
|
||||
if err := lp.Shutdown(ctx); err != nil {
|
||||
errs = append(errs, &providerErr{"log", err})
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
return shutdown, nil
|
||||
}
|
||||
|
||||
// --- trace exporter ---
|
||||
|
||||
type logTraceExporter struct{ logger logging.Logger }
|
||||
|
||||
func (e *logTraceExporter) ExportSpans(_ context.Context, spans []sdktrace.ReadOnlySpan) error {
|
||||
for _, s := range spans {
|
||||
e.logger.Info("otel: span",
|
||||
"name", s.Name(),
|
||||
"trace_id", s.SpanContext().TraceID(),
|
||||
"span_id", s.SpanContext().SpanID(),
|
||||
"parent_span_id", s.Parent().SpanID(),
|
||||
"kind", s.SpanKind(),
|
||||
"duration_ms", s.EndTime().Sub(s.StartTime()).Milliseconds(),
|
||||
"status", s.Status().Code,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *logTraceExporter) Shutdown(_ context.Context) error { return nil }
|
||||
|
||||
// --- metric exporter ---
|
||||
|
||||
type logMetricExporter struct{ logger logging.Logger }
|
||||
|
||||
func (e *logMetricExporter) Temporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
|
||||
return metricdata.CumulativeTemporality
|
||||
}
|
||||
|
||||
func (e *logMetricExporter) Aggregation(k sdkmetric.InstrumentKind) sdkmetric.Aggregation {
|
||||
return sdkmetric.DefaultAggregationSelector(k)
|
||||
}
|
||||
|
||||
func (e *logMetricExporter) Export(_ context.Context, rm *metricdata.ResourceMetrics) error {
|
||||
for _, sm := range rm.ScopeMetrics {
|
||||
for _, m := range sm.Metrics {
|
||||
e.logMetric(m)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *logMetricExporter) logMetric(m metricdata.Metrics) {
|
||||
switch d := m.Data.(type) {
|
||||
case metricdata.Sum[int64]:
|
||||
for _, dp := range d.DataPoints {
|
||||
e.logger.Info("otel: metric", "name", m.Name, "kind", "sum", "value", dp.Value, "unit", m.Unit)
|
||||
}
|
||||
case metricdata.Sum[float64]:
|
||||
for _, dp := range d.DataPoints {
|
||||
e.logger.Info("otel: metric", "name", m.Name, "kind", "sum", "value", dp.Value, "unit", m.Unit)
|
||||
}
|
||||
case metricdata.Gauge[int64]:
|
||||
for _, dp := range d.DataPoints {
|
||||
e.logger.Info("otel: metric", "name", m.Name, "kind", "gauge", "value", dp.Value, "unit", m.Unit)
|
||||
}
|
||||
case metricdata.Gauge[float64]:
|
||||
for _, dp := range d.DataPoints {
|
||||
e.logger.Info("otel: metric", "name", m.Name, "kind", "gauge", "value", dp.Value, "unit", m.Unit)
|
||||
}
|
||||
case metricdata.Histogram[int64]:
|
||||
for _, dp := range d.DataPoints {
|
||||
e.logger.Info("otel: metric", "name", m.Name, "kind", "histogram", "count", dp.Count, "sum", dp.Sum, "unit", m.Unit)
|
||||
}
|
||||
case metricdata.Histogram[float64]:
|
||||
for _, dp := range d.DataPoints {
|
||||
e.logger.Info("otel: metric", "name", m.Name, "kind", "histogram", "count", dp.Count, "sum", dp.Sum, "unit", m.Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *logMetricExporter) ForceFlush(_ context.Context) error { return nil }
|
||||
func (e *logMetricExporter) Shutdown(_ context.Context) error { return nil }
|
||||
|
||||
// --- log exporter ---
|
||||
|
||||
type logLogExporter struct{ logger logging.Logger }
|
||||
|
||||
func (e *logLogExporter) Export(_ context.Context, records []sdklog.Record) error {
|
||||
for _, r := range records {
|
||||
body := r.Body().AsString()
|
||||
sev := r.Severity()
|
||||
switch {
|
||||
case sev >= otellog.SeverityError:
|
||||
e.logger.Error("otel: log", nil, "body", body, "severity", r.SeverityText())
|
||||
case sev >= otellog.SeverityWarn:
|
||||
e.logger.Warn("otel: log", "body", body, "severity", r.SeverityText())
|
||||
default:
|
||||
e.logger.Info("otel: log", "body", body, "severity", r.SeverityText())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *logLogExporter) ForceFlush(_ context.Context) error { return nil }
|
||||
func (e *logLogExporter) Shutdown(_ context.Context) error { return nil }
|
||||
Reference in New Issue
Block a user