Skip to content

feat(tracing): Span.startChild + getActiveSpan + honoTracing helper#9

Open
stackbilt-admin wants to merge 1 commit intomainfrom
feat/startchild-active-span-hono-helper
Open

feat(tracing): Span.startChild + getActiveSpan + honoTracing helper#9
stackbilt-admin wants to merge 1 commit intomainfrom
feat/startchild-active-span-hono-helper

Conversation

@stackbilt-admin
Copy link
Copy Markdown
Member

Summary

Three dogfood-driven ergonomics lifts surfaced while instrumenting `tarotscript-worker` (tarotscript#163).

  1. `Span.startChild(name, attrs?)` — convenience method that saves ~3 lines of plumbing per child span (ergonomics: Span.startChild() + Hono-aware middleware helper #3)
  2. `getActiveSpan()` + `Tracer.runWithSpan()` — AsyncLocalStorage-backed active span so deep code creates child spans without threading parent context through function signatures (feat: AsyncLocalStorage-backed active span context #4)
  3. `honoTracing()` middleware — batteries-included helper that wraps the full root-span lifecycle every dogfood worker was hand-rewriting (ergonomics: Span.startChild() + Hono-aware middleware helper #3)

Why it matters

Every Stackbilt worker instrumented with this library (stackbilt-web, edge-auth, tarotscript, img-forge, aegis, pro-tier customer workers) was rewriting the same ~30-line middleware. Centralizing it means bug fixes propagate automatically, new workers integrate in 2 lines instead of 30, and deep code (interpreters, LLM providers, scaffold materializers) can now participate in tracing without taking a hard dep on observability.

AsyncLocalStorage notes

Requires `nodejs_als` or `nodejs_compat` compat flag in `wrangler.toml`. When unavailable, `getActiveSpan()` returns `undefined` and `runWithSpan` becomes an identity wrapper — no crashes, just no-op.

honoTracing features

  • Wraps `next()` in `tracer.runWithSpan(rootSpan, ...)` for automatic active-span scoping
  • Records `http.*` attributes, `recordError()` on throw, `setStatus('error')` on 5xx
  • Publishes root span to `c.get('rootSpan')` (or custom `contextKey`)
  • Uses `ctx.waitUntil(Promise.allSettled([tracer.flush(), metrics.flush()]))` so ingest doesn't block the response
  • Options: `skip`, `attributes`, `spanNamer`, `contextKey`
  • Accepts minimal `HonoTracingMonitoring` shape — decoupled from full `createMonitoring` surface

Test plan

  • typecheck clean
  • 82/82 tests pass (71 existing redact + 11 new ergonomics)
  • startChild trace-id inheritance, parent linkage, attributes
  • runWithSpan/getActiveSpan sync, async, deep-stack scenarios
  • honoTracing: null-safety, skip, root span publication, active-span scoping, 5xx → error, thrown error → recordError, custom spanNamer + attributes
  • Deploy to tarotscript-worker dogfood, verify child spans appear in dashboards without threading
  • Follow-up: docs+release: publish to npm + ship Quickstart for Hono workers #5 (npm publish + Hono quickstart docs) — deferred until dogfooded in prod

Depends on

Independent of #7 (span-drops fix in #8) — they touch different methods, can land in either order.

Closes #3
Closes #4

🤖 Generated with Claude Code

Three dogfood-driven ergonomics lifts, all surfaced while instrumenting
tarotscript-worker (tarotscript#163). Closes worker-observability#3, #4.

What's new

1. Span.startChild(name, attributes?)
   - Convenience wrapper around `this.tracer.startSpan(name, { parent })`
   - Saves ~3 lines of mechanical plumbing at every child-span call site
   - NoOpSpan override returns itself so sampled-out spans are still safe

2. AsyncLocalStorage-backed active span context
   - Tracer.runWithSpan(span, fn) scopes the span for a code region
   - getActiveSpan() returns the scoped span anywhere in the call stack,
     across async boundaries, without threading a parent through every
     function signature
   - Import guarded via @ts-expect-error; node:async_hooks is pulled in
     at runtime by the Workers `nodejs_als` / `nodejs_compat` flag.
     When ALS is unavailable the storage is null and both runWithSpan
     and getActiveSpan degrade gracefully (identity wrapper / undefined)
   - This is the scalability lever for deep instrumentation — library
     code (interpreters, LLM providers, scaffold materializers) can now
     create child spans without taking a hard dep on observability

3. honoTracing() — batteries-included Hono middleware
   - Wraps the full root-span lifecycle pattern every dogfood worker
     was rewriting: span start, HTTP attributes, recordError on throw,
     error status on 5xx, publish to c.get('rootSpan'), automatic
     active-span scope via runWithSpan, and ctx.waitUntil-based flush
     of tracer+metrics so ingest doesn't block the response
   - Options: skip predicate, extra attributes callback, custom
     spanNamer, contextKey override
   - Accepts a minimal HonoTracingMonitoring shape ({ tracer, metrics })
     so it doesn't couple to the full createMonitoring bundle
   - Leaves the existing tracingMiddleware in place; honoTracing is the
     replacement for dogfood consumers

Tests

Added src/__tests__/tracing-ergonomics.test.ts (11 tests):
- startChild: trace-id inheritance, parent-span linkage, attributes
- runWithSpan/getActiveSpan: synchronous, async, deep-stack scenarios
- honoTracing: null-safety, skip predicate, root span publication,
  active-span scoping, 5xx → error, thrown error → recordError,
  custom spanNamer + attributes

82/82 tests passing (71 existing redact + 11 new ergonomics).

Exports added to src/index.ts:
- honoTracing, getActiveSpan
- HonoTracingMonitoring, HonoTracingOptions types

Closes #3
Closes #4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: AsyncLocalStorage-backed active span context ergonomics: Span.startChild() + Hono-aware middleware helper

1 participant