Skip to content

ergonomics: Span.startChild() + Hono-aware middleware helper #3

@stackbilt-admin

Description

@stackbilt-admin

Context

Surfaced while dogfooding @stackbilt/worker-observability in tarotscript-worker (Stackbilt-dev/tarotscript#163). Every dogfood target repeats the same ~30 lines of boilerplate to get root-span-per-request, plus awkward child-span creation. These patterns should live in the library.

Pain point 1 — child spans require .getContext() + { parent } wrapping

Every child-span call site becomes:

```ts
const child = tracer.startSpan('scaffold.classify', {
parent: rootSpan.getContext(),
attributes: { ... },
});
// ... work ...
child.end();
```

Three lines of mechanical plumbing per child span. In tarotscript's `/run` handler I have 4 child spans, so ~12 lines of `rootSpan.getContext()` + `parent:` passing that carries zero signal.

Proposed ergonomic: Span.startChild(name, attrs?)

```ts
const child = rootSpan.startChild('scaffold.classify', { ... });
```

Implementation is trivial — `Span.startChild()` becomes a method that calls `this.tracer.startSpan(name, { parent: this.getContext(), attributes })` on the same tracer. It's the ergonomic shape every OpenTelemetry SDK converges on and it makes the child-span relationship textually obvious at the call site.

Pain point 2 — no Hono-aware middleware helper

The library exports `tracingMiddleware(tracer)` but:

  • It doesn't know about Hono's Variables context → can't stash the root span for downstream handlers to create child spans against
  • It doesn't use `c.executionCtx.waitUntil(tracer.flush())` → blocks response on HTTP ingest
  • It doesn't record errors via `span.recordError()` on thrown exceptions
  • It doesn't set `span.setStatus('error')` on 5xx responses

Every dogfood worker ends up writing this ~30-line middleware themselves:

```ts
app.use('*', async (c, next) => {
const obs = getMonitoring(c.env);
if (!obs?.tracer) return next();
const url = new URL(c.req.url);
const span = obs.tracer.startTrace(`${c.req.method} ${url.pathname}`, {
'http.method': c.req.method,
'http.target': url.pathname,
'http.host': url.host,
});
c.set('rootSpan', span);
try {
await next();
span.setAttributes({ 'http.status_code': c.res.status });
if (c.res.status >= 500) span.setStatus('error');
} catch (err) {
span.recordError(err as Error);
span.setStatus('error');
throw err;
} finally {
span.end();
c.executionCtx.waitUntil(Promise.allSettled([
obs.tracer.flush(),
obs.metrics.flush(),
]));
}
});
```

Proposed ergonomic: honoTracing(monitoring, options?)

```ts
import { honoTracing } from '@stackbilt/worker-observability/hono';

app.use('*', honoTracing(getMonitoring(c.env)));
// downstream handlers:
const rootSpan = c.get('rootSpan');
const child = rootSpan?.startChild('scaffold.classify');
```

Options could include:

  • `skip?: (c) => boolean` — e.g., skip health checks or static asset routes
  • `attributes?: (c) => Record<string, any>` — add tenant_id, user_id, route_pattern
  • `spanNamer?: (c) => string` — custom naming (default: `${method} ${pathname}`)

Impact on users

Every Stackbilt worker instrumented with this library (stackbilt-web, edge-auth, tarotscript, img-forge, aegis, and future pro-tier customer workers) copy-pastes the same middleware. Centralizing it means:

  • Bug fixes propagate to everyone automatically (e.g., if we discover a `waitUntil` edge case)
  • New workers go from zero to traces in 2 lines instead of 30
  • Consistency across dashboards — same attribute names, same error-recording semantics

Related

  • Sibling issue for AsyncLocalStorage active span context — will file separately
  • Sibling issue for npm publishing + quickstart docs — will file separately

These three issues together represent the friction a dogfood user hits on first instrumentation. Fixing them turns the library from "read the source to figure it out" into "3-line integration."

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions