Skip to content

systemd v260+ report framework

systemd metrics for Go and OpenTelemetry

Publish your daemon's metrics under /run/systemd/report/ in a few lines, then export every source on the host, systemd's own included, as OTLP or Prometheus, each attributed to the unit that produced it. Pure Go, no cgo.

go get github.com/rossedman/sdmetrics@latest
sdmetrics-exporter -once
SOURCE              UNIT
io.systemd.Manager  init.scope
io.systemd.Network  systemd-networkd.service
io.example.Queue    queued.service

FAMILY                               VALUE
Manager.UnitsByTypeTotal{type=service} 142
Network.OperationalState{eth0}  "routable"
Queue.JobsProcessed{result=ok}         9120

Thirty seconds of sdmetrics

package main

import (
    "context"
    "log"
    "os"
    "os/signal"

    "github.com/rossedman/sdmetrics/provider"
)

func main() {
    p, err := provider.New("io.example.Queue") // (1)!
    if err != nil {
        log.Fatal(err)
    }
    depth := p.Gauge("Depth", provider.Help("Jobs waiting"))
    jobs := p.Counter("JobsProcessed", provider.Labels("result")) // (2)!

    depth.Set(42)
    jobs.With("ok").Inc()

    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
    defer stop()
    if err := p.Serve(ctx); err != nil { // (3)!
        log.Fatal(err)
    }
}
  1. The provider name is the socket's file name: /run/systemd/report/io.example.Queue. Families are published as io.example.Queue.Depth and io.example.Queue.JobsProcessed.
  2. Labels become the fields of each value. Updates are atomic and do not allocate.
  3. Serve binds the socket (mode 0660), answers io.systemd.Metrics and removes the socket when the context ends.
$ systemd-report metrics io.example.Queue
$ varlinkctl call --more /run/systemd/report/io.example.Queue io.systemd.Metrics.List '{}'

Your daemon is now a report source like io.systemd.Manager. Anything that reads systemd's report framework reads it too.

# Print every source once, with the unit that owns it.
sdmetrics-exporter -once

# Push OTLP to a collector and serve Prometheus.
sdmetrics-exporter -otlp.endpoint otel-gateway:4317 -otlp.insecure \
                   -prometheus.listen 127.0.0.1:9558 -naming otel
receivers:
  systemdreport:
    collection_interval: 30s
    naming: otel

service:
  pipelines:
    metrics:
      receivers: [systemdreport]
      exporters: [otlp]
s := scrape.New(scrape.Options{})
res, err := s.Scrape(ctx)
if err != nil {
    return err // scrape.ErrNoReportDir before systemd v260
}
for _, src := range res.Sources {
    fmt.Println(src.Name, src.Unit, len(src.Families), src.Err)
}
~27 msto scrape 50 sources, 5,000 values
~38 nsper Gauge.Set, 0 allocations
1.5systemd-analyze security score of the packaged unit
3ways out: OTLP · Prometheus · Collector
0cgo, agents or D-Bus

What you get

  • A provider SDK


    Gauges, counters and string metrics with labels and objects, served as io.systemd.Metrics on a report socket. Lock-free, allocation-free updates.

    Provider SDK

  • Bridges for existing code


    Already instrumented? Publish a Prometheus registry or an OpenTelemetry SDK reader through a provider in two lines.

    Prometheus · OpenTelemetry

  • A host exporter


    sdmetrics-exporter scrapes every source on an interval and pushes OTLP, serves /metrics, or both. Shipped as deb and rpm with a hardened unit.

    Exporter

  • A Collector receiver


    systemdreport brings the same scrape into the OpenTelemetry Collector, built with mdatagen and scraperhelper.

    Receiver

  • Unit identity


    Every source is attributed to its systemd unit, from peer credentials and cgroups, or from the manager for socket-activated sources.

    Unit identity

  • A documented OTel mapping


    One resource per source, service.name from the unit, counters with proper start times, and an optional OTel naming mode.

    Mapping

Why systemd's report framework?

Before v260, every daemon that wanted metrics opened its own HTTP port, picked its own names and left you to work out which service a port belonged to. systemd v260 adds a report framework: a service serves the io.systemd.Metrics Varlink interface on a socket under /run/systemd/report/, and any consumer lists the directory to find every source on the host.

Before: a port per daemon

flowchart TB
    P["Prometheus"] -- ":9100" --> A["node_exporter"]
    P -- ":9558" --> B["your daemon"]
    P -- "?" --> C["systemd itself"]

After: one directory per host

flowchart TB
    E["sdmetrics-exporter<br/>or systemdreport receiver"] -- "list" --> D[("/run/systemd/report/")]
    D --> M["io.systemd.Manager"]
    D --> N["io.systemd.Network"]
    D --> Q["io.example.Queue<br/>(your daemon)"]

No ports, no service discovery, and identity for free: the socket's peer credentials tell the scraper which unit answers. Learn the model in Core concepts.