mirror of
https://github.com/luxfi/metric.git
synced 2026-10-30 16:19:50 +00:00
exporter_otelzap.go was the only file in the root package importing go.opentelemetry.io, and it made the root package link 34 OpenTelemetry SDK packages. Nothing else in the root package wants them: the registry, the text encoder and the ZAP exporter are pure luxfi/zap. That cost was not paid by the root package alone. github.com/zap-proto/zip imports this module for the registry, EncodeText and NewZAPExporter, and passes on whatever the root package links to each of the 125 programs built on it. The ruling is native ZAP only, so nothing a plugin inherits may carry the OpenTelemetry SDK. The adapter is now package github.com/luxfi/metric/otel, and the two symbols it exports lose the prefixes that only existed to disambiguate them inside the root package: metric.NewOTelZAPExporter(cfg) -> otel.New(cfg) *metric.OTelZAPExporter -> *otel.Exporter The configuration type stays metric.ZAPExporterConfig, since it belongs to the ZAP exporter this adapter builds on. No alias is left behind in the root package — one that re-imported the moved package would link the SDK again and undo the change. Callers of the adapter add the otel import and drop the prefixes. Proof, rather than a grep for import lines: go list -deps . | grep -c go.opentelemetry.io -> 0 go list -deps ./otel | grep -c go.opentelemetry.io -> 34 0 holds under the default tags, -tags metrics and -tags metrics_noop alike. A program importing zip v1.25.0 against this module resolves 0 as well, measured with a replace directive. The conversion had no tests, so the move now pins it: counter and gauge shapes, the drop of an aggregation these shapes cannot carry, and the arithmetic that matters — OpenTelemetry reports a count per bucket where Prometheus reports a running total. The metrics_noop defaults are untouched; recording stays the default and the no-op stays opt-in. Build, vet and test -race are green under all three tag sets. Co-authored-by: Hanzo Dev <[email protected]>
159 lines
6.1 KiB
Go
159 lines
6.1 KiB
Go
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package otel ships metrics recorded by the OpenTelemetry metric SDK over ZAP.
|
|
//
|
|
// This adapter lives in its own package because importing it costs the caller
|
|
// the entire OpenTelemetry metric SDK and everything beneath it — several dozen
|
|
// packages that the registry, the text encoder and the ZAP exporter in the root
|
|
// package never touch. Plugins such as github.com/zap-proto/zip import the root
|
|
// package for exactly those three things and pass whatever it links on to every
|
|
// program they are built into, so the root package stays free of OpenTelemetry
|
|
// and a caller that wants this adapter asks for it by name.
|
|
//
|
|
// The ZAP exporter accepts MetricFamily values, the shape a Prometheus gatherer
|
|
// yields. A service instrumented with the OpenTelemetry metric SDK produces
|
|
// ResourceMetrics instead, so this package translates one into the other. Such
|
|
// a service can then ship over ZAP without an OTLP exporter, and therefore
|
|
// without protobuf or gRPC, which otlpmetrichttp pulls in as surely as
|
|
// otlpmetricgrpc does.
|
|
//
|
|
// Histograms decompose into classic bucket/count/sum samples rather than a
|
|
// sketch: ZAP carries Prometheus shapes, and the o11y query plane already reads
|
|
// them.
|
|
package otel
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/metric"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
|
"go.opentelemetry.io/otel/sdk/metric/metricdata"
|
|
)
|
|
|
|
// Exporter adapts the OpenTelemetry metric SDK to the ZAP exporter.
|
|
type Exporter struct {
|
|
zap *metric.ZAPExporter
|
|
}
|
|
|
|
var _ sdkmetric.Exporter = (*Exporter)(nil)
|
|
|
|
// New returns an sdkmetric.Exporter that ships over ZAP.
|
|
//
|
|
// Install it on a periodic reader exactly as an OTLP exporter would be:
|
|
//
|
|
// exp, err := otel.New(metric.ZAPExporterConfig{
|
|
// Endpoint: "o11y.hanzo.svc:4317", AppName: "ingress",
|
|
// })
|
|
// mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exp)))
|
|
func New(cfg metric.ZAPExporterConfig) (*Exporter, error) {
|
|
z, err := metric.NewZAPExporter(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Exporter{zap: z}, nil
|
|
}
|
|
|
|
// Temporality is cumulative for every instrument: ZAP carries Prometheus
|
|
// shapes, and a delta sum has no meaning to a Prometheus reader.
|
|
func (e *Exporter) Temporality(sdkmetric.InstrumentKind) metricdata.Temporality {
|
|
return metricdata.CumulativeTemporality
|
|
}
|
|
|
|
// Aggregation follows the SDK default, which yields explicit-bucket histograms
|
|
// — the only histogram these shapes can express.
|
|
func (e *Exporter) Aggregation(kind sdkmetric.InstrumentKind) sdkmetric.Aggregation {
|
|
return sdkmetric.DefaultAggregationSelector(kind)
|
|
}
|
|
|
|
func (e *Exporter) Export(ctx context.Context, rm *metricdata.ResourceMetrics) error {
|
|
families := make([]*metric.MetricFamily, 0, 16)
|
|
for _, scope := range rm.ScopeMetrics {
|
|
for _, m := range scope.Metrics {
|
|
if fam := toFamily(m); fam != nil {
|
|
families = append(families, fam)
|
|
}
|
|
}
|
|
}
|
|
if len(families) == 0 {
|
|
return nil
|
|
}
|
|
return e.zap.Export(ctx, families)
|
|
}
|
|
|
|
// ForceFlush has nothing to flush: Export sends synchronously.
|
|
func (e *Exporter) ForceFlush(context.Context) error { return nil }
|
|
|
|
func (e *Exporter) Shutdown(ctx context.Context) error { return e.zap.Shutdown(ctx) }
|
|
|
|
// toFamily converts one OpenTelemetry metric. Unsupported aggregations return
|
|
// nil rather than an empty family, so a metric these shapes cannot carry is
|
|
// dropped visibly at one place instead of arriving empty.
|
|
func toFamily(m metricdata.Metrics) *metric.MetricFamily {
|
|
switch d := m.Data.(type) {
|
|
case metricdata.Sum[int64]:
|
|
return &metric.MetricFamily{Name: m.Name, Help: m.Description, Type: metric.MetricTypeCounter,
|
|
Metrics: numberPoints(d.DataPoints, func(v int64) float64 { return float64(v) })}
|
|
case metricdata.Sum[float64]:
|
|
return &metric.MetricFamily{Name: m.Name, Help: m.Description, Type: metric.MetricTypeCounter,
|
|
Metrics: numberPoints(d.DataPoints, func(v float64) float64 { return v })}
|
|
case metricdata.Gauge[int64]:
|
|
return &metric.MetricFamily{Name: m.Name, Help: m.Description, Type: metric.MetricTypeGauge,
|
|
Metrics: numberPoints(d.DataPoints, func(v int64) float64 { return float64(v) })}
|
|
case metricdata.Gauge[float64]:
|
|
return &metric.MetricFamily{Name: m.Name, Help: m.Description, Type: metric.MetricTypeGauge,
|
|
Metrics: numberPoints(d.DataPoints, func(v float64) float64 { return v })}
|
|
case metricdata.Histogram[int64]:
|
|
return &metric.MetricFamily{Name: m.Name, Help: m.Description, Type: metric.MetricTypeHistogram,
|
|
Metrics: histogramPoints(d.DataPoints, func(v int64) float64 { return float64(v) })}
|
|
case metricdata.Histogram[float64]:
|
|
return &metric.MetricFamily{Name: m.Name, Help: m.Description, Type: metric.MetricTypeHistogram,
|
|
Metrics: histogramPoints(d.DataPoints, func(v float64) float64 { return v })}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func numberPoints[N int64 | float64](pts []metricdata.DataPoint[N], f func(N) float64) []metric.Metric {
|
|
out := make([]metric.Metric, 0, len(pts))
|
|
for _, p := range pts {
|
|
out = append(out, metric.Metric{Labels: labels(p.Attributes), Value: metric.MetricValue{Value: f(p.Value)}})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func histogramPoints[N int64 | float64](pts []metricdata.HistogramDataPoint[N], f func(N) float64) []metric.Metric {
|
|
out := make([]metric.Metric, 0, len(pts))
|
|
for _, p := range pts {
|
|
// Prometheus buckets are cumulative; OpenTelemetry counts are per-bucket.
|
|
buckets := make([]metric.Bucket, 0, len(p.Bounds))
|
|
var running uint64
|
|
for i, bound := range p.Bounds {
|
|
if i < len(p.BucketCounts) {
|
|
running += p.BucketCounts[i]
|
|
}
|
|
buckets = append(buckets, metric.Bucket{UpperBound: bound, CumulativeCount: running})
|
|
}
|
|
out = append(out, metric.Metric{
|
|
Labels: labels(p.Attributes),
|
|
Value: metric.MetricValue{
|
|
SampleCount: p.Count,
|
|
SampleSum: f(p.Sum),
|
|
Buckets: buckets,
|
|
},
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func labels(set attribute.Set) []metric.LabelPair {
|
|
out := make([]metric.LabelPair, 0, set.Len())
|
|
for it := set.Iter(); it.Next(); {
|
|
kv := it.Attribute()
|
|
out = append(out, metric.LabelPair{Name: string(kv.Key), Value: fmt.Sprint(kv.Value.AsInterface())})
|
|
}
|
|
return out
|
|
}
|