#Tracing
Alepha traces itself. Every request, job run, cron tick and queue or topic message opens a trace, and the work inside it (actions, page loaders, queries, outbound calls, cache reads, lock waits) becomes its spans. There is no code to write: you choose where the spans go.
- In development, devtools shows each trace as a waterfall.
- On Node and Bun, set
OTEL_EXPORTER_OTLP_ENDPOINTand spans are posted to any OpenTelemetry backend. - On Cloudflare Workers, spans join Cloudflare's own trace, beside the D1, KV and fetch spans the platform records.
With none of these, nothing is recorded and tracing costs a length check per instrumented call.
#What is traced
| Span | Opened by | Kind |
|---|---|---|
GET /users/:id |
a server request, named by route pattern | server |
action createUser |
each $action call, HTTP or local |
internal |
loader home, render home |
each $page loader, and the SSR render |
internal |
job sendDigest |
a $job run |
internal |
cron sendDigest |
a cron tick | internal |
queue jobs, topic orders |
a queue or topic message | consumer |
GET api.example.com |
each HttpClient call |
client |
select users, insert ... |
each SQL statement a repository runs | internal |
query |
a raw DatabaseProvider.execute() |
internal |
cache get user-profile |
a $cache read, write or invalidate |
internal |
lock acquire |
a $lock waiting for its lock |
internal |
A request span ends when the response has been sent, so a streamed page counts its whole stream. A 5xx marks it failed; a 4xx does not. A thrown error is recorded on the span it escaped, with its type and message.
One repository method can call another (getById reads through findMany),
but each statement is one span, named by its SQL verb and table. Five
select order_items spans under one parent is the shape of an N+1, and
devtools flags it.
Queries, cache operations and lock waits are only recorded inside a trace: a statement run at boot or by a poll loop opens no trace of its own.
#Log lines carry their trace
While a span is active, every log entry carries traceId and spanId. A JSON
log line opens its trace, and devtools shows a span's own log lines under it.
1{2 "level": "INFO",3 "message": "Charging card",4 "traceId": "0af7651916cd43dd8448eb211c80319c",5 "spanId": "b7ad6b7169203331"6}
#Naming a business step
Tracer.span() wraps a block of your own code in a span. It nests under
whatever is running, ends when the callback returns or settles, and records
and rethrows an error.
1import { $inject } from "alepha"; 2import { Tracer } from "alepha/tracing"; 3 4class Checkout { 5 protected readonly tracer = $inject(Tracer); 6 7 async pay(order: { id: string; items: string[] }) { 8 return this.tracer.span("checkout pay", async (span) => { 9 span.setAttribute("order.items", order.items.length);10 // ... the work11 return order.id;12 });13 }14}
Attributes leave the process. Record counts and categories, never personal data, tokens or ids that identify a person.
#Across services
HttpClient sends a W3C traceparent header with every call made inside a
trace, and an Alepha server receiving one continues the caller's trace. Two
Alepha services on Node or Bun therefore form a single trace.
A queue or topic message carries the producer's traceparent in its envelope.
The consumer starts a trace of its own, linked to the producer's span rather
than nested under it: a message can be handled long after the request that sent
it ended.
#Exporting on Node and Bun
Point the OTLP exporter at a collector or a hosted backend. Every mainstream one accepts OTLP over HTTP directly, so no collector is needed.
OTEL_EXPORTER_OTLP_ENDPOINT=https://otlp.example.com/otlp
OTEL_EXPORTER_OTLP_HEADERS=authorization=Basic%20dXNlcjpwYXNz
Spans are posted as JSON to <endpoint>/v1/traces, in batches of 512 or every
five seconds, and whatever is queued is sent when the app stops. The service
name is the app's name (APP_NAME). A failed export is logged once and dropped:
it never retries, never slows a request and never throws into your code.
#Sampling
TRACING_SAMPLE_RATIO (from 0 to 1, default 1) is the share of new traces
recorded. It is decided once, at the root, and a trace continued from another
service follows that service's decision. At the volumes most apps see, keep the
default: record everything.
#On Cloudflare Workers
On a Worker, Alepha's spans are Cloudflare custom spans: the request span, your actions and queries sit in the same tree as the platform's D1, KV, R2, Durable Object and fetch spans, and log lines carry the ids the dashboard shows. There is no exporter of Alepha's own; Cloudflare's OTLP destinations export the whole trace.
Traces are opt in, because Cloudflare bills them as observability ingestion.
Set TRACING_SAMPLE_RATIO when you build:
TRACING_SAMPLE_RATIO=1 alepha build --runtime workerd
The build then writes observability.traces into wrangler.jsonc, with that
ratio as its sampling rate. Without the variable, no traces key is written and
nothing is recorded. An app that declares its own observability block keeps it
untouched.
The build also records the ratio in the artifact's manifest.json, so a deploy
that regenerates wrangler.jsonc from a built artifact, as a Lore deploy does,
keeps traces on. A deployed copy can set TRACING_SAMPLE_RATIO in its own
environment too: that value wins over the recorded one, which turns traces on,
off (0) or to another rate without a rebuild.
No traceparent leaves a Worker: Cloudflare does not say whether a request is
sampled, and its trace ids are not propagated outside Cloudflare.
#Reading traces in devtools
Under alepha dev, open devtools and pick Traces: the last two hundred
traces of the app, newest first, with a filter for failed ones. A trace opens
as a waterfall; a span shows its timing, attributes, error and log lines. A
parent of five or more same-name spans carries an N+1 warning.
#What is never recorded
- Bound SQL values, the SQL text, or a
whereobject. - A raw URL, a query string or a header: a request span holds the route
pattern, an
HttpClientspan the host. - A cache key: a cache span names the cache.
- A computed lock name:
lock.nameis recorded only when the name is a static string.
#Time on Workers
On Cloudflare Workers the clock only advances across I/O. A span that only computes measures 0 ms there, however long it took. Durations of spans that wait on a query or a fetch are accurate.