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-logging3description: 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'.4---5
6# JoelClaw Observability + Logging
7
8Prevent silent failure by default. Observability is not optional polish: it is part of done.
9
10## Non-Negotiable Rules
11
121. Use the canonical event contract only.
13 - `packages/system-bus/src/observability/otel-event.ts`
14 - `packages/system-bus/src/observability/emit.ts`
15 - `packages/system-bus/src/observability/store.ts`
162. Worker/Inngest code emits through `emitOtelEvent` or `emitMeasuredOtelEvent`.
173. Gateway code emits through `emitGatewayOtel`.
184. Internal ingestion goes through `POST /observability/emit` (`packages/system-bus/src/serve.ts`), not ad-hoc writes.
195. Never treat `console.log` as primary observability. Keep structured events as source of truth.
206. High-cardinality values go in `metadata`, not in facet fields (`source`, `component`, `level`, `success`).
217. Failures must set `success: false` with a meaningful `error`.
228. Verify delivery to the sinks required by current telemetry configuration. Inspect mirror enablement first; a retired or optional mirror does not block unrelated work.
239. In Inngest durable functions, any "emit once" telemetry must live inside `step.run(...)` to avoid replay duplication after resume.
24
25## Event Conventions
26
27- `source`: subsystem (`worker`, `gateway`, `webhook`, `memory`, `verification`, etc.)
28- `component`: stable module/service name (`check-system-health`, `redis-channel`, `observe`)
29- `action`: stable dotted action (`system.health.checked`, `events.immediate_telegram`)
30- `metadata`: request IDs, deployment IDs, function IDs, session IDs, payload identifiers
31- `duration_ms`: include for timed operations
32
33Use event-per-hop (wide event style): one context-rich event for each major boundary/operation, not scattered string logs.
34
35## Implementation Workflow
36
371. Identify the boundary being changed.
38 - Inngest function, gateway channel, webhook route, API route, background job, sync step.
392. Add success and failure envelopes.
40 - Start + completion for long tasks, or a single completion event for short tasks.
413. Include operational and business context in `metadata`.
42 - Example: function id, event id, provider, queue depth, affected resource id.
434. Keep severity useful.
44 - `debug/info` for normal activity, `warn` for degraded but recoverable, `error/fatal` for failures.
455. Run verification gates before finishing.
46
47For full checklists and command recipes, read `references/implementation-checklist.md`.
48
49## Quick Patterns
50
51### Worker / Inngest timed operation
52
53```typescript
54import { emitMeasuredOtelEvent } from "../../observability/emit";
55
56await emitMeasuredOtelEvent(
57 {
58 level: "info",
59 source: "worker",
60 component: "content-sync",
61 action: "content_sync.run",
62 metadata: { trigger: event.name },
63 },
64 async () => {
65 await runSync();
66 }
67);
68```
69
70### Gateway emission
71
72```typescript
73import { emitGatewayOtel } from "../observability";
74
75await emitGatewayOtel({
76 level: "error",
77 component: "redis-channel",
78 action: "events.immediate_telegram",
79 success: false,
80 error: "telegram_send_failed",
81 metadata: { sessionId, queueDepth },
82});
83```
84
85### CLI emission
86
87Use `--metadata` for JSON context. There is no `--attributes` flag.
88
89```bash
90joelclaw otel emit "task.completed" \
91 --source system \
92 --component skills \
93 --success true \
94 --metadata '{"session":"NimbleBadger","task":"install wzrrd-publish skill"}'
95```
96
97## Definition of Done
98
99- Structured OTEL events added for the changed path.
100- No direct feature-level writes to Typesense/Convex for observability data.
101- Smoke probe passes (`scripts/otel-smoke.sh`).
102- `joelclaw otel list` and `joelclaw otel stats` show expected behavior.
103- New failure modes are queryable by `source`, `component`, and `action`.
104
105## Inngest Replay + Hang Triage
106
107Use this when step code appears to run but runs remain `RUNNING`/`CANCELLED` with `Finalization` errors.
108
1091. Inspect run trace first.
110
111```bash
112joelclaw run <run-id>
113```
114
115Look for `errors.Finalization.stack` containing `Unable to reach SDK URL`.
116
1172. Confirm whether this is true network reachability or worker-side blocking.
118
119```bash
120joelclaw inngest status
121joelclaw logs worker --lines 200
122joelclaw logs errors --lines 200
123```
124
1253. Check for replay-noise in OTEL.
126
127If 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`.
128
129```bash
130joelclaw otel search "manifest.archive.prereqs-passed" --hours 1
131```
132
1334. Treat `Unable to reach SDK URL` as an ambiguous symptom.
134
135It 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.
136
137## Helper Script
138
139Use `scripts/otel-smoke.sh` for a fast end-to-end probe:
140
141```bash
142./skills/o11y-logging/scripts/otel-smoke.sh verification o11y-skill probe.emit
143```
144
145## Key Files
146
147- `packages/system-bus/src/observability/otel-event.ts`
148- `packages/system-bus/src/observability/emit.ts`
149- `packages/system-bus/src/observability/store.ts`
150- `packages/system-bus/src/serve.ts`
151- `packages/gateway/src/observability.ts`
152- `packages/system-bus/src/inngest/functions/check-system-health.ts`
153- `packages/cli/src/commands/otel.ts`
154- `apps/web/app/api/otel/route.ts`