Bridging OpenTelemetry¶
Code instrumented with the OpenTelemetry Go SDK joins systemd's report
framework through a ManualReader: add it to the MeterProvider, wrap it in
otelbridge.New, register it on a provider.
reader := sdkmetric.NewManualReader()
otel.SetMeterProvider(sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)))
p, err := provider.New("io.example.Api")
if err != nil {
return err
}
p.Register(otelbridge.New(reader))
go p.Serve(ctx)
Each List or Describe call collects from the reader once. Other readers,
such as a periodic OTLP push, can sit next to it on the same
MeterProvider: the bridge doesn't replace your pipeline, it adds systemd as
another consumer.
Cumulative temporality
The reader must use cumulative temporality, the SDK default. Delta sums are published as gauges, since they reset on every collection, and delta histograms as gauges too.
A complete example¶
examples/otel-bridge is a worker instrumented with the OpenTelemetry SDK:
// Command otel-bridge is a worker instrumented with the OpenTelemetry Go
// SDK that publishes its metrics through systemd's report framework. A
// ManualReader on the MeterProvider is all the bridge needs; other
// readers, such as an OTLP push, can sit next to it.
//
// go run ./examples/otel-bridge -dir /tmp/report
// go run ./cmd/sdmetrics-exporter -once -dir /tmp/report
package main
import (
"context"
"flag"
"log"
"math/rand/v2" // nosemgrep: go.lang.security.audit.crypto.math_random.math-random-used
"os"
"os/signal"
"syscall"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"github.com/rossedman/sdmetrics/provider"
"github.com/rossedman/sdmetrics/provider/otelbridge"
)
func main() {
dir := flag.String("dir", provider.DefaultDir, "report directory")
flag.Parse()
reader := sdkmetric.NewManualReader()
mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader))
defer mp.Shutdown(context.Background())
otel.SetMeterProvider(mp)
p, err := provider.New("io.example.Worker", provider.WithDir(*dir), provider.WithSocketMode(0o666))
if err != nil {
log.Fatal(err)
}
p.Register(otelbridge.New(reader))
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := p.Serve(ctx); err != nil {
log.Fatal(err)
}
}()
// Existing instrumentation, unchanged.
meter := otel.Meter("example/worker")
tasks, _ := meter.Int64Counter("worker.tasks", metric.WithDescription("Tasks processed"))
dur, _ := meter.Float64Histogram("worker.task.duration", metric.WithUnit("s"), metric.WithDescription("Task duration"))
log.Printf("publishing %s", p.Path())
for ctx.Err() == nil {
d := time.Duration(rand.IntN(500)) * time.Millisecond
time.Sleep(d)
kind := []string{"resize", "encode"}[rand.IntN(2)]
tasks.Add(ctx, 1, metric.WithAttributes(attribute.String("kind", kind)))
dur.Record(ctx, d.Seconds(), metric.WithAttributes(attribute.String("kind", kind)))
}
}
go run ./examples/otel-bridge -dir /tmp/report &
go run ./cmd/sdmetrics-exporter -once -dir /tmp/report -include io.example.Worker
How instruments map¶
| OpenTelemetry data | systemd families | Fields |
|---|---|---|
Sum, monotonic, cumulative (Counter, ObservableCounter) |
counter name |
attributes |
Sum, non-monotonic or delta (UpDownCounter, ...) |
gauge name |
attributes |
Gauge (Gauge, ObservableGauge) |
gauge name |
attributes |
| Histogram, explicit buckets | counter name_bucket, counter name_sum, counter name_count |
attributes; cumulative le on buckets |
| Exponential histogram | counter name_sum, counter name_count |
attributes |
| Summary | gauge name, counter name_sum, counter name_count |
attributes; quantile on name |
Attribute values are published as their string form. Buckets are made
cumulative, with a final le="+Inf", as in the Prometheus layout.
Exponential histograms keep only their sum and count: the report framework
has no way to carry their buckets.
How names map¶
systemd accepts letters, digits and single underscores in a metric name, so the bridge rewrites OpenTelemetry names:
- Dots and other characters systemd does not accept become
_. - The units
s,msandByare appended as_seconds,_millisecondsand_bytes, unless the name already ends that way.
| Instrument | Unit | Published as |
|---|---|---|
http.server.request.duration |
s |
io.example.Api.http_server_request_duration_seconds |
worker.tasks |
— | io.example.Worker.worker_tasks |
worker.task.duration |
s |
io.example.Worker.worker_task_duration_seconds_bucket (+ _sum, _count) |
cache.size |
By |
io.example.Api.cache_size_bytes |
otelbridge.Name(name, unit) returns the name the bridge uses, if you need
it elsewhere:
With the exporter's naming: otel
the unit suffix moves back into the unit field on the way out:
io.example.Api.http_server_request_duration_seconds is exported as
io.example.api.http_server_request_duration with unit s. The round trip
recovers the unit; the original dots in the instrument name become
underscores.
Histograms on the Prometheus endpoint
Bridged histogram families are plain counters to systemd. The exporter's
Prometheus endpoint keeps their _bucket, _sum and _count names in
both naming modes, so histogram_quantile works on the _bucket series.
Next: Scraping from Go.