celld can record traces and logs for the requests it serves. Telemetry is off
by default and costs nothing when off. CELLD_OTEL=1 writes Parquet files
under the telemetry/ prefix of the fleet bucket, which DuckDB can query
directly. CELLD_OTEL=http://collector:4318 sends the same data to an
OpenTelemetry collector.
The schema is version v0-unstable, so column names can change before a
stable release. Each file carries the version in the object metadata name
celld-schema, or celld_schema on an az:// bucket.
| variable | default | effect |
|---|---|---|
CELLD_OTEL |
0 |
0 disables telemetry. 1 writes Parquet to the fleet bucket. An HTTP(S) collector base URL selects OTLP/HTTP protobuf. |
CELLD_OTEL_BUCKET |
the fleet bucket | A different bucket for the Parquet files, on the same endpoint and credentials. |
CELLD_OTEL_RETENTION |
30d |
celld deletes telemetry files older than this. none disables the deletion, so your own lifecycle rules can control the data. |
CELLD_OTEL_FLUSH_MS |
300000 |
celld writes a Parquet file after this many milliseconds of buffered events. |
CELLD_OTEL_FLUSH_BYTES |
5242880 |
The estimated buffered bytes that trigger a flush before the interval ends. |
OTEL_TRACES_SAMPLER |
parentbased_always_on |
A standard sampler name. traceidratio with OTEL_TRACES_SAMPLER_ARG records a fraction of the traces. |
OTEL_EXPORTER_OTLP_HEADERS |
unset | A comma-separated list of name=value headers for the collector. |
OTEL_EXPORTER_OTLP_TIMEOUT |
10000 |
The collector request timeout in milliseconds. |
OTEL_SERVICE_NAME |
celld |
The service name in the exported resource. |
The bucket sink requires CELLD_BUCKET. The OTLP sink does not.
A collector URL must have no query or fragment. celld appends /v1/traces and
/v1/logs to its path and ignores OTEL_EXPORTER_OTLP_ENDPOINT.
CELLD_OTEL_SINK is removed, and a node with it set does not start. Put the
collector base URL in CELLD_OTEL, or keep CELLD_OTEL=1 for the bucket.
celld records a span for each stateless Worker request, each cell event (a
fetch, an alarm, an RPC, a WebSocket message), each outbound fetch(), and
each cell start. A span carries the request id, the cell, the isolate, the
queue wait, the outbound URL and status, and the known durability facts.
Each console.log line becomes a log record with the trace id and span id of
its handler, across await. The record carries the console method's severity
in severity_number and severity_text (OTLP fields and Parquet columns). The
body holds only the message.
| method | severity number | severity text |
|---|---|---|
console.debug |
5 |
DEBUG |
console.log, console.info |
9 |
INFO |
console.warn |
13 |
WARN |
console.error |
17 |
ERROR |
Log files from earlier celld versions have no severity columns. Read a mix of
versions with union_by_name = true.
celld reads the W3C traceparent header on incoming requests and sends it on
outbound fetch(). A malformed header starts a new trace. A Worker call to a
Durable Object stays in one trace.
The sampler decides at the start of a request, and an unsampled request
records nothing. A ratio of 0 records no traces and 1 records all. An
intermediate ratio makes the same trace-id decision on each node. celld keeps
a valid incoming context that the sampler rejects: an outbound fetch() or
Durable Object call keeps the trace id, uses a new span id, and keeps the
sampled flag clear.
Under load, telemetry sheds before requests do, and celld counts what it sheds. celld records no metrics yet.
INSTALL httpfs; LOAD httpfs;
CREATE SECRET celld_telemetry (
TYPE s3, KEY_ID '...', SECRET '...',
ENDPOINT 's3.example.com', URL_STYLE 'path'
);
CREATE VIEW traces AS SELECT * FROM
read_parquet('s3://YOUR-BUCKET/telemetry/traces/*/*/*/*/*/*.parquet');
CREATE VIEW logs AS SELECT * FROM
read_parquet('s3://YOUR-BUCKET/telemetry/logs/*/*/*/*/*/*.parquet');
-- The slowest requests.
SELECT name, duration_us, trace_id FROM traces
ORDER BY duration_us DESC LIMIT 20;
-- The error lines.
SELECT time_unix_us, body FROM logs WHERE severity_number >= 17;
-- Every log line, inside the span that wrote it.
SELECT l.body, t.name, t.duration_us FROM logs l
JOIN traces t ON l.trace_id = t.trace_id AND l.span_id = t.span_id;A non-AWS S3-compatible endpoint needs URL_STYLE 'path'. A plain-HTTP
endpoint also needs USE_SSL false.
Files are partitioned by node and hour:
telemetry/traces/<node>/<yyyy>/<mm>/<dd>/<hh>/<id>.parquet.
celld writes one Parquet file per flush, at 5 minutes
(CELLD_OTEL_FLUSH_MS=300000) or 5 MiB of estimated buffered events
(CELLD_OTEL_FLUSH_BYTES=5242880), whichever comes first. The event that
reaches the target can take the batch past it.
Keep the defaults if you run no compaction job. They produce large files, but
data can arrive up to 5 minutes late. CELLD_OTEL_FLUSH_MS=5000 gives a
five-second interval, plus upload or collector delay. A short interval makes
many small files and slows queries within hours, so start the compaction job
first. For a near-live view, use the OTLP sink with the same short interval.
The OTLP sink makes at most five attempts per batch on a transient failure
(HTTP 408, 429, 502, 503, or 504). It uses exponential backoff with jitter and
honors Retry-After, with each delay capped at 30 seconds. A permanent
refusal drops the batch.
The exporter holds one retrying batch, and the input channel holds 8192 new events. When the channel is full, celld drops and counts new telemetry, so request handling continues.
The retention sweep runs at startup and six hours after each completed sweep.
celld does not compact its own files. Run a compaction job on a maintenance node once an hour, for the hour that just ended. Do not compact the current hour, because a node still writes to it.
COPY (
SELECT * FROM
read_parquet('s3://YOUR-BUCKET/telemetry/traces/<node>/2026/08/09/22/*.parquet')
ORDER BY start_unix_us
) TO 's3://YOUR-BUCKET/telemetry/traces/<node>/2026/08/09/22/compacted.parquet'
(FORMAT parquet, COMPRESSION zstd);Delete the source files after DuckDB writes the compacted file.