Observability (OpenTelemetry)
@zuke/otel is a zero-runtime-dependency plugin that exports a
run as OpenTelemetry traces
and metrics over OTLP/HTTP JSON — hand-rolled, so core stays
OTel-free. Register it on a run and each state transition ships to a
collector: one run span with a child span per executed target, plus
run/suspend/outcome counters.
The otel() plugin
// jsr:@zuke/otel
export function otel(configure?: Configure<OtelSettings>): Plugin
Called with no arguments — or with no endpoint configured, whether by
setter or by OTEL_EXPORTER_OTLP_ENDPOINT — the plugin is
inert, so it's safe to register unconditionally.
Otherwise configure gets an OtelSettings with
fluent setters, each returning this:
endpoint(url): this // base; /v1/traces & /v1/metrics appended
tracesEndpoint(url): this // verbatim
metricsEndpoint(url): this // verbatim
serviceName(name): this
header(name, value): this
headers(map): this
resourceAttribute(name, value): this
resourceAttributes(map): this
timeout(duration): this // "5s"; default 10s What it exports
The plugin observes onRunStateChange against the durable
RunRecord:
| Signal | When | Shape |
|---|---|---|
| trace | run settles | Run span (createdAt→updatedAt) + one child span per executed target. |
zuke.run.started | fresh run begins | Counter, tagged zuke.build / zuke.root_target. |
zuke.run.suspended | run parks at a .waitsFor gate | Counter per waiting target, tagged zuke.build / zuke.target / trigger. |
zuke.runs | run settles | Counter tagged outcome = succeeded/failed/cancelled. |
Run span attributes: zuke.run.id, zuke.build,
zuke.root_target, zuke.actor,
zuke.run.status. Target span attributes:
zuke.target, zuke.target.status; a failed
target's span is marked error with its redacted message. Metrics are
OTLP delta Sums. Only executed targets get a span, and a run is
exported once (the dedup set is capped at 1024).
Setter ↔ env config
Every setter has an env fallback, so the same registration works locally and in CI; an explicit setter always wins over its env var.
| Setter | Env var |
|---|---|
.endpoint(url) | OTEL_EXPORTER_OTLP_ENDPOINT (base; /v1/traces & /v1/metrics appended) |
.tracesEndpoint(url) | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT (verbatim) |
.metricsEndpoint(url) | OTEL_EXPORTER_OTLP_METRICS_ENDPOINT (verbatim) |
.serviceName(name) | OTEL_SERVICE_NAME (else "zuke") |
.header(s) | OTEL_EXPORTER_OTLP_HEADERS (k1=v1,k2=v2) |
.resourceAttribute(s) | OTEL_RESOURCE_ATTRIBUTES |
.timeout(duration) | — (setter only; "5s" shape, default 10s) |
Trace continuity across suspend/resume
A run that suspends and resumes in a different process still lands as
one trace: the trace id is
SHA-256(runId) truncated to 16 bytes, and each target span
id is a stable hash of (runId, target). Every process
derives the same ids, so the whole run — including the gap while it was
suspended — reassembles into one complete, gap-spanning trace.
This requires a state store: the exported spans and counters come from the durable run record, so a store-less build exports nothing.
Setup examples
import { run } from "jsr:@zuke/core";
import { otel } from "jsr:@zuke/otel";
await run(MyBuild, {
plugins: [
otel((s) => s.endpoint("http://localhost:4318").serviceName("my-build").header("authorization", "Bearer …")),
],
}); // Config entirely from OTEL_* env — same registration works locally and in CI:
await run(MyBuild, { plugins: [otel()] });
Export is best-effort: a down or slow collector never fails the build
beyond the configured timeout. Output is secret-free — the same
redacted projection zuke runs show prints. Available as
the new @zuke/otel package on JSR.