NewConsole bootstraps the OTel SDK with three logz-backed exporters: - Trace: WithSyncer, one log line per closed span (immediate, no batch) - Metric: PeriodicReader (60s), flushed on shutdown - OTel log: BatchProcessor, for third-party libs using OTel log API ConsoleConfig requires only ServiceName — no OTLP endpoint needed. Adds logz v1.0.1 as direct dependency; module tier bumped 1 → 2.
184 lines
5.9 KiB
Go
184 lines
5.9 KiB
Go
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/go/logz"
|
|
)
|
|
|
|
// ConsoleConfig holds the minimum OTel configuration needed for console/dev mode.
|
|
// Only service identity fields are required — no OTLP endpoint.
|
|
type ConsoleConfig struct {
|
|
ServiceName string `env:"OTEL_SERVICE_NAME,required"`
|
|
ServiceVersion string `env:"OTEL_SERVICE_VERSION" envDefault:"unknown"`
|
|
Environment string `env:"OTEL_ENVIRONMENT" envDefault:"development"`
|
|
}
|
|
|
|
// NewConsole bootstraps the OTel SDK with logz-backed exporters for local development.
|
|
// All signals (traces, metrics, OTel log records) are emitted as structured logz log
|
|
// lines instead of being sent to a collector.
|
|
//
|
|
// Drop-in alternative to [New] for development environments.
|
|
//
|
|
// Warning: do not wire the OTel slog bridge alongside NewConsole — routing logz through
|
|
// the OTel log API and back through logLogExporter creates a feedback loop.
|
|
func NewConsole(ctx context.Context, logger logz.Logger, cfg ConsoleConfig) (func(context.Context) error, error) {
|
|
res, err := resource.Merge(
|
|
resource.Default(),
|
|
resource.NewWithAttributes(
|
|
semconv.SchemaURL,
|
|
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 logz.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 logz.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 logz.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 }
|