JoelClaw Observability + Logging
Prevent silent failure by default. Observability is not optional polish: it is part of done.
Non-Negotiable Rules
- Use the canonical event contract only.
packages/system-bus/src/observability/otel-event.ts
packages/system-bus/src/observability/emit.ts
packages/system-bus/src/observability/store.ts
- Worker/Inngest code emits through
emitOtelEvent or emitMeasuredOtelEvent.
- Gateway code emits through
emitGatewayOtel.
- Internal ingestion goes through
POST /observability/emit (packages/system-bus/src/serve.ts), not ad-hoc writes.
- Never treat
console.log as primary observability. Keep structured events as source of truth.
- High-cardinality values go in
metadata, not in facet fields (source, component, level, success).
- Failures must set
success: false with a meaningful error.
- Verify delivery to the sinks required by current telemetry configuration. Inspect mirror enablement first; a retired or optional mirror does not block unrelated work.
- In Inngest durable functions, any "emit once" telemetry must live inside
step.run(...) to avoid replay duplication after resume.
Event Conventions
source: subsystem (worker, gateway, webhook, memory, verification, etc.)
component: stable module/service name (check-system-health, redis-channel, observe)
action: stable dotted action (system.health.checked, events.immediate_telegram)
metadata: request IDs, deployment IDs, function IDs, session IDs, payload identifiers
duration_ms: include for timed operations
Use event-per-hop (wide event style): one context-rich event for each major boundary/operation, not scattered string logs.
Implementation Workflow
- Identify the boundary being changed.
- Inngest function, gateway channel, webhook route, API route, background job, sync step.
- Add success and failure envelopes.
- Start + completion for long tasks, or a single completion event for short tasks.
- Include operational and business context in
metadata.
- Example: function id, event id, provider, queue depth, affected resource id.
- Keep severity useful.
debug/info for normal activity, warn for degraded but recoverable, error/fatal for failures.
- Run verification gates before finishing.
For full checklists and command recipes, read references/implementation-checklist.md.
Quick Patterns
Worker / Inngest timed operation
import { emitMeasuredOtelEvent } from "../../observability/emit";
await emitMeasuredOtelEvent(
{
level: "info",
source: "worker",
component: "content-sync",
action: "content_sync.run",
metadata: { trigger: event.name },
},
async () => {
await runSync();
}
);
Gateway emission
import { emitGatewayOtel } from "../observability";
await emitGatewayOtel({
level: "error",
component: "redis-channel",
action: "events.immediate_telegram",
success: false,
error: "telegram_send_failed",
metadata: { sessionId, queueDepth },
});
CLI emission
Use --metadata for JSON context. There is no --attributes flag.
joelclaw otel emit "task.completed" \
--source system \
--component skills \
--success true \
--metadata '{"session":"NimbleBadger","task":"install wzrrd-publish skill"}'
Definition of Done
- Structured OTEL events added for the changed path.
- No direct feature-level writes to Typesense/Convex for observability data.
- Smoke probe passes (
scripts/otel-smoke.sh).
joelclaw otel list and joelclaw otel stats show expected behavior.
- New failure modes are queryable by
source, component, and action.
Inngest Replay + Hang Triage
Use this when step code appears to run but runs remain RUNNING/CANCELLED with Finalization errors.
- Inspect run trace first.
joelclaw run <run-id>
Look for errors.Finalization.stack containing Unable to reach SDK URL.
- Confirm whether this is true network reachability or worker-side blocking.
joelclaw inngest status
joelclaw logs worker --lines 200
joelclaw logs errors --lines 200
- Check for replay-noise in OTEL.
If an action that should emit once (for example manifest.archive.prereqs-passed) appears hundreds of times in one run window, move that emit into its own step.run.
joelclaw otel search "manifest.archive.prereqs-passed" --hours 1
- Treat
Unable to reach SDK URL as an ambiguous symptom.
It can indicate ingress problems, but in practice it can also happen when a function handler blocks on local IO/dependencies long enough that finalization cannot complete.
Helper Script
Use scripts/otel-smoke.sh for a fast end-to-end probe:
./skills/o11y-logging/scripts/otel-smoke.sh verification o11y-skill probe.emit
Key Files
packages/system-bus/src/observability/otel-event.ts
packages/system-bus/src/observability/emit.ts
packages/system-bus/src/observability/store.ts
packages/system-bus/src/serve.ts
packages/gateway/src/observability.ts
packages/system-bus/src/inngest/functions/check-system-health.ts
packages/cli/src/commands/otel.ts
apps/web/app/api/otel/route.ts
1---
2name: o11y-logging
3displayName: O11y Logging
4description: "Implement and verify joelclaw observability on every change so failures cannot stay silent. Use when adding/updating Inngest functions, gateway channels, webhook providers, APIs, workers, or any pipeline step. Enforces canonical OTEL contract, storage path, and verification gates. Triggers on: 'o11y', 'observability', 'logging', 'otel', 'instrument this', 'silent failure', 'add telemetry', 'log this function'."
5version: 1.1.0
6author: Joel Hooks
7tags: [joelclaw, observability, logging, o11y, otel, typesense, convex, inngest, gateway]
8---
9
10# JoelClaw Observability + Logging
11
12Prevent silent failure by default. Observability is not optional polish: it is part of done.
13
14## Non-Negotiable Rules
15
161. Use the canonical event contract only.
17 - `packages/system-bus/src/observability/otel-event.ts`
18 - `packages/system-bus/src/observability/emit.ts`
19 - `packages/system-bus/src/observability/store.ts`
202. Worker/Inngest code emits through `emitOtelEvent` or `emitMeasuredOtelEvent`.
213. Gateway code emits through `emitGatewayOtel`.
224. Internal ingestion goes through `POST /observability/emit` (`packages/system-bus/src/serve.ts`), not ad-hoc writes.
235. Never treat `console.log` as primary observability. Keep structured events as source of truth.
246. High-cardinality values go in `metadata`, not in facet fields (`source`, `component`, `level`, `success`).
257. Failures must set `success: false` with a meaningful `error`.
268. Verify delivery to the sinks required by current telemetry configuration. Inspect mirror enablement first; a retired or optional mirror does not block unrelated work.
279. In Inngest durable functions, any "emit once" telemetry must live inside `step.run(...)` to avoid replay duplication after resume.
28
29## Event Conventions
30
31- `source`: subsystem (`worker`, `gateway`, `webhook`, `memory`, `verification`, etc.)
32- `component`: stable module/service name (`check-system-health`, `redis-channel`, `observe`)
33- `action`: stable dotted action (`system.health.checked`, `events.immediate_telegram`)
34- `metadata`: request IDs, deployment IDs, function IDs, session IDs, payload identifiers
35- `duration_ms`: include for timed operations
36
37Use event-per-hop (wide event style): one context-rich event for each major boundary/operation, not scattered string logs.
38
39## Implementation Workflow
40
411. Identify the boundary being changed.
42 - Inngest function, gateway channel, webhook route, API route, background job, sync step.
432. Add success and failure envelopes.
44 - Start + completion for long tasks, or a single completion event for short tasks.
453. Include operational and business context in `metadata`.
46 - Example: function id, event id, provider, queue depth, affected resource id.
474. Keep severity useful.
48 - `debug/info` for normal activity, `warn` for degraded but recoverable, `error/fatal` for failures.
495. Run verification gates before finishing.
50
51For full checklists and command recipes, read `references/implementation-checklist.md`.
52
53## Quick Patterns
54
55### Worker / Inngest timed operation
56
57```typescript
58import { emitMeasuredOtelEvent } from "../../observability/emit";
59
60await emitMeasuredOtelEvent(
61 {
62 level: "info",
63 source: "worker",
64 component: "content-sync",
65 action: "content_sync.run",
66 metadata: { trigger: event.name },
67 },
68 async () => {
69 await runSync();
70 }
71);
72```
73
74### Gateway emission
75
76```typescript
77import { emitGatewayOtel } from "../observability";
78
79await emitGatewayOtel({
80 level: "error",
81 component: "redis-channel",
82 action: "events.immediate_telegram",
83 success: false,
84 error: "telegram_send_failed",
85 metadata: { sessionId, queueDepth },
86});
87```
88
89### CLI emission
90
91Use `--metadata` for JSON context. There is no `--attributes` flag.
92
93```bash
94joelclaw otel emit "task.completed" \
95 --source system \
96 --component skills \
97 --success true \
98 --metadata '{"session":"NimbleBadger","task":"install wzrrd-publish skill"}'
99```
100
101## Definition of Done
102
103- Structured OTEL events added for the changed path.
104- No direct feature-level writes to Typesense/Convex for observability data.
105- Smoke probe passes (`scripts/otel-smoke.sh`).
106- `joelclaw otel list` and `joelclaw otel stats` show expected behavior.
107- New failure modes are queryable by `source`, `component`, and `action`.
108
109## Inngest Replay + Hang Triage
110
111Use this when step code appears to run but runs remain `RUNNING`/`CANCELLED` with `Finalization` errors.
112
1131. Inspect run trace first.
114
115```bash
116joelclaw run <run-id>
117```
118
119Look for `errors.Finalization.stack` containing `Unable to reach SDK URL`.
120
1212. Confirm whether this is true network reachability or worker-side blocking.
122
123```bash
124joelclaw inngest status
125joelclaw logs worker --lines 200
126joelclaw logs errors --lines 200
127```
128
1293. Check for replay-noise in OTEL.
130
131If an action that should emit once (for example `manifest.archive.prereqs-passed`) appears hundreds of times in one run window, move that emit into its own `step.run`.
132
133```bash
134joelclaw otel search "manifest.archive.prereqs-passed" --hours 1
135```
136
1374. Treat `Unable to reach SDK URL` as an ambiguous symptom.
138
139It can indicate ingress problems, but in practice it can also happen when a function handler blocks on local IO/dependencies long enough that finalization cannot complete.
140
141## Helper Script
142
143Use `scripts/otel-smoke.sh` for a fast end-to-end probe:
144
145```bash
146./skills/o11y-logging/scripts/otel-smoke.sh verification o11y-skill probe.emit
147```
148
149## Key Files
150
151- `packages/system-bus/src/observability/otel-event.ts`
152- `packages/system-bus/src/observability/emit.ts`
153- `packages/system-bus/src/observability/store.ts`
154- `packages/system-bus/src/serve.ts`
155- `packages/gateway/src/observability.ts`
156- `packages/system-bus/src/inngest/functions/check-system-health.ts`
157- `packages/cli/src/commands/otel.ts`
158- `apps/web/app/api/otel/route.ts`