OpenTelemetry mapping¶
The otelmap package defines one mapping, used by sdmetrics-exporter and
the systemdreport receiver alike. A Mapper turns each scrape.Result into
a Batch: one resource per report source, plus one resource for
sdmetrics' own self-metrics. otelmap.ToSDK converts a batch for the
OpenTelemetry Go SDK; the receiver converts it to Collector pdata.
Types¶
| systemd type | OpenTelemetry | Value |
|---|---|---|
| gauge | Gauge | the number |
| counter | Sum, monotonic, cumulative | the number; start time is the first observation |
| string | Gauge | 1, with the string in the systemd.value attribute |
object (systemd v262) |
not mapped | dropped, counted in sdmetrics.metrics.dropped{reason="untyped"} |
Integer values (scrape.KindInt) become integer data points, others double
data points. When the Go SDK conversion meets a metric with both, it uses
doubles for all of its points.
A family with no values in a scrape produces no metric. Families without a known type never reach the mapper: the scraper drops and counts them.
Resource attributes¶
Each source's resource carries:
| Attribute | Value | Present |
|---|---|---|
host.name |
the hostname | always (override with Options.HostName) |
host.id |
/etc/machine-id (or /var/lib/dbus/machine-id) |
when readable (override with Options.HostID) |
your resource: keys |
as configured, sorted by key | when configured |
systemd.report.source |
the socket's file name, such as io.systemd.Manager |
always |
systemd.unit.name |
the unit owning the socket, such as init.scope |
when known, or set with units: |
process.pid |
the socket's peer PID (int) | when the scraper resolved the unit itself and the source is not socket-activated |
service.name |
derived from the unit, see below | always |
service.name is, in order of preference:
- the source's entry in
service_names:; systemdforinit.scope;- the unit name without
.serviceor.socket, and without a template instance:getty@tty1.service→getty; - any other unit name as it is;
- the source name, when the unit is unknown.
The self-metrics resource carries host.name, host.id, your resource:
keys and service.name="sdmetrics".
Proposed attributes
systemd.report.source, systemd.unit.name, systemd.object and
systemd.value are proposals in OpenTelemetry semantic-convention style.
They are not part of the conventions yet; proposing them is on the
roadmap.
Point attributes¶
| Attribute | From |
|---|---|
| each field | the value's fields, keys sorted, values as strings |
systemd.object |
the value's object, when present |
systemd.value |
the string, for string families |
io.systemd.Manager.NRestarts object=systemd-networkd.service value=0
→ Sum name=io.systemd.Manager.NRestarts attributes={systemd.object=systemd-networkd.service} value=0
io.systemd.Manager.UnitActiveState object=systemd-networkd.service value="active"
→ Gauge attributes={systemd.object=systemd-networkd.service, systemd.value=active} value=1
Naming modes¶
naming (otelmap.Options.Naming) decides metric names. Descriptions always
come from Describe.
raw(default,NamingRaw)- systemd's family names, unchanged:
io.systemd.Manager.NRestarts. No unit. otel(NamingOTel)-
OpenTelemetry style, by
otelmap.OTelName:- the namespace (everything before the last dot) is lower-cased, and a
leading
io.systemdbecomessystemd; dashes become_; - the field name becomes snake_case, keeping acronyms together;
- a trailing
Totalword is dropped; - a trailing unit word moves into the unit field.
- the namespace (everything before the last dot) is lower-cased, and a
leading
| Unit word | OTel unit |
|---|---|
Seconds |
s |
Milliseconds |
ms |
Microseconds |
us |
Nanoseconds |
ns |
Bytes |
By |
Ratio |
1 |
Percent |
% |
Steps 3 and 4 apply only when a word would remain, so io.example.A.Seconds
stays io.example.a.seconds.
| systemd family | otel name |
Unit |
|---|---|---|
io.systemd.Manager.UnitsByTypeTotal |
systemd.manager.units_by_type |
|
io.systemd.Manager.NRestarts |
systemd.manager.n_restarts |
|
io.systemd.Network.IPv4AddressState |
systemd.network.ipv4_address_state |
|
io.example.Api.RequestLatencySeconds |
io.example.api.request_latency |
s |
io.example.Api.ResponseBytesTotal |
io.example.api.response |
By |
io.example.my-app.CPUUsageRatio |
io.example.my_app.cpu_usage |
1 |
io.example.A.HTTPServer |
io.example.a.http_server |
|
io.example.queue.jobs_processed_total |
io.example.queue.jobs_processed |
Self-metrics are always named sdmetrics.*, with units, in both modes.
Counter start times and resets¶
Report sources send cumulative counters without a start time. The mapper keeps one per series (source, family and point attributes):
- A series seen for the first time starts now: its first point has start time equal to its timestamp.
- If the value goes down, or the source's peer PID changes (the provider restarted), the series restarts: the start time becomes the current scrape's.
- A series missing from a scrape keeps its start time for 10 scrapes, so a source that fails briefly doesn't reset its counters.
Gauges have no start time. sdmetrics.metrics.dropped starts when the
mapper was created.
Because of this, keep one Mapper per pipeline and reuse it for every
scrape. A mapper is safe for concurrent use.
Scope¶
| Producer | Scope name | Version |
|---|---|---|
sdmetrics-exporter, otelmap.ToSDK |
github.com/rossedman/sdmetrics |
the exporter's version (the scopeVersion argument) |
systemdreport receiver |
github.com/rossedman/sdmetrics/receiver/systemdreportreceiver |
the Collector's build version |
In Go¶
m := otelmap.NewMapper(otelmap.Options{
Naming: otelmap.NamingOTel,
Resource: map[string]string{"deployment.environment": "prod"},
ServiceNames: map[string]string{"io.example.Queue": "queue"},
Units: map[string]string{"io.example.Queue": "queued.service"},
})
b := m.Map(res) // res from scrape.Scraper.Scrape
for _, r := range b.Resources {
fmt.Println(r.Source, len(r.Metrics)) // r.Source is "" for the self-metrics
}
rms := otelmap.ToSDK(b, "v0.1.0") // []*metricdata.ResourceMetrics
_ = rms