Skip to content

Observability

View as Markdown

StoryShelf exports OpenTelemetry traces and metrics to your own collector, and correlates its JSON logs with those traces. Nothing leaves your deployment unless you configure an endpoint — without one, observability is a zero-overhead noop.

The word “telemetry” is deliberately reserved for a possible future opt-in feature reporting anonymous usage back to StoryShelf. Everything on this page exports only to endpoints you configure.

Terminal window
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318 \
OTEL_SERVICE_NAME=storyshelf-prod \
nub run serve

Upload a build and you will see one trace per upload: the HTTP request span parenting the async capture.job span (extract → render → persist), with every log line carrying the same trace_id. The consumer continues the same trace via the job row.

VariableDefaultMeaning
OTEL_EXPORTER_OTLP_ENDPOINTunset (disabled)OTLP/HTTP base URL, e.g. http://collector:4318.
OTEL_SERVICE_NAMEstoryshelfService name in spans and metrics.
OTEL_EXPORTER_OTLP_HEADERSunsetExtra OTLP headers, k=v,k2=v2 (collector auth).
OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARGalways-onProduction suggestion: traceidratio + 0.1.
STORYSHELF_OTEL_ENABLEDautoExplicit false kill-switch.
STORYSHELF_OTEL_METRICS_INTERVAL_MS60000Metric export interval.

Scaffolded servers (storyshelf server init) already include the initialization preamble and log correlation — you only add the endpoint.

Spans: http.server per request, capture.job per capture with extract / persist-statics / render / persist phases, db.* and storage.* per adapter operation, http.client for outbound calls (git hosts, webhooks, OIDC). Span names never contain IDs or paths.

Metrics: capture.job.duration, capture.jobs.completed/failed, db.operation.duration, storage.operation.duration, http.server.request.duration — all low-cardinality by rule.

Logs: stdout JSON unchanged, plus trace_id/span_id on every line inside a span, so logs join with traces in your backend. A signed Storybook URL token in a request path is replaced by [redacted] in request logs and in the span’s url.full attribute.

Traces need a collector; Server-Timing headers need only a browser. Set serverTiming: true in ShelfConfig to opt in (off by default — omit it in production if you prefer no timing headers):

const app = createShelfApp({
database,
storage,
config: { scratchDir: dataDir, serverTiming: true },
});

Every response then carries e.g. Server-Timing: total;dur=42.1, db;dur=12.5, storage;dur=3.2 — total matches the http.server span, db/storage are roll-ups of the adapter calls in that request (per-operation detail stays in traces). No OpenTelemetry SDK is required; with one configured, the names line up with the db.* / storage.* span families on purpose.

Background capture work never appears in the header (there is no response to attach it to). Per-build timing breakdowns live in the attempt log history instead (GET /api/v1/projects/:id/builds/:buildId/attempts/:attemptNo/logs).

For the full reference (span/metric catalogs, sampling, Deno, troubleshooting), see docs/observability.md in the repository and ADR 0015.