You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Second of three ergonomics issues surfaced while dogfooding on Stackbilt-dev/tarotscript#163. Sibling: #3 (Span.startChild + Hono middleware).
Pain point
Child spans require access to a parent span. Today, the parent has to be threaded through every function call that wants to create a child. This works for shallow handlers but fails catastrophically for deep instrumentation.
Concrete example from tarotscript
The `POST /run` handler wants child spans on:
`scaffold.classify` (in the handler itself — easy)
`tarotscript.interpret` (calls `run(source)` which enters the TarotScript interpreter)
Oracle inference calls (fire inside the interpreter via an injected function)
The interpreter lives in `src/interpreter/interpreter.ts`, 5 call-stack layers deep from the handler. To create an "oracle.inference" child span at that depth, I'd have to:
The interpreter knows nothing about the root span. The observability library handles parent resolution via ALS. The language stays pure.
Why this matters for the platform
This is the scalability lever for deep instrumentation. Without it:
Library users can only instrument shallow handler code
Dashboards show incomplete waterfalls (interpret phase is a black box)
Third-party customer workers that want to instrument their own code hit the same wall
With it:
`getActiveSpan()` becomes the universal "I'm inside a traced request, add a span" pattern
Any library (TarotScript interpreter, LLM providers, scaffold materializer) can optionally create spans without taking a dependency on observability — just import `getActiveSpan` as an optional peer and no-op when it returns undefined
Customer workers can instrument their own deep code without threading context
Caveats
Cloudflare Workers compatibility: AsyncLocalStorage requires the `nodejs_als` compat flag in `wrangler.toml`. The library should document this requirement and degrade gracefully (return `undefined` from `getActiveSpan()`) when ALS is not available
Memory: Spans held in ALS for the duration of the request — negligible for normal request lifetimes
Testing: ALS semantics need test coverage for nested spans, async boundaries, error paths
Context
Second of three ergonomics issues surfaced while dogfooding on Stackbilt-dev/tarotscript#163. Sibling: #3 (Span.startChild + Hono middleware).
Pain point
Child spans require access to a parent span. Today, the parent has to be threaded through every function call that wants to create a child. This works for shallow handlers but fails catastrophically for deep instrumentation.
Concrete example from tarotscript
The `POST /run` handler wants child spans on:
The interpreter lives in `src/interpreter/interpreter.ts`, 5 call-stack layers deep from the handler. To create an "oracle.inference" child span at that depth, I'd have to:
That's a hard no — language stays pure.
Proposed ergonomic: AsyncLocalStorage active span
```ts
import { AsyncLocalStorage } from 'node:async_hooks';
// (Workers runtime supports AsyncLocalStorage since ~2023; compatibility flag 'nodejs_als')
// Library internals
const activeSpanStorage = new AsyncLocalStorage();
export class Tracer {
runWithSpan(span: Span, fn: () => T): T {
return activeSpanStorage.run(span, fn);
}
}
export function getActiveSpan(): Span | undefined {
return activeSpanStorage.getStore();
}
```
Usage from the Hono middleware
```ts
app.use('*', honoTracing(obs, {
// middleware wraps `next()` in tracer.runWithSpan(rootSpan, ...) automatically
}));
```
Usage from deep code (no threading)
```ts
// Inside the interpreter's oracle call, 5 layers down:
import { getActiveSpan } from '@stackbilt/worker-observability';
async function inferenceFn(prompt: string) {
const span = getActiveSpan()?.startChild('oracle.inference', {
'oracle.provider': 'cerebras',
'oracle.prompt_tokens': estimateTokens(prompt),
});
try {
const result = await providers.complete(prompt);
span?.setAttributes({ 'oracle.completion_tokens': result.usage.completion_tokens });
return result;
} finally {
span?.end();
}
}
```
The interpreter knows nothing about the root span. The observability library handles parent resolution via ALS. The language stays pure.
Why this matters for the platform
This is the scalability lever for deep instrumentation. Without it:
With it:
Caveats
Related