12 views
Monkey-patching http.request or wrapping every pg.query leaks into behavior and breaks on library upgrades. Node’s diagnostics_channel lets you subscribe to named channels (undici:request:*, custom app channels) with near-zero overhead when no subscribers exist — production-safe instrumentation without patching.
⚡ TL;DR: Publish on stable channel names from shared clients; subscribe once at process boot for metrics/traces; keep subscribers sync and tiny; avoid capturing huge bodies. Pair with Node.js Event Loop Lag p99 and OpenTelemetry for LLMs.
Node Diagnostic Channels: production guidance
Teams reach forModule.prototype wrappers or require-cache hijacks because they are familiar. In production those patches:
- Break when a library switches from
httptoundicior from callbacks to promises - Double-instrument under Nest/Express middleware stacks that already wrap the same APIs
- Capture full request bodies and leak PII into metrics backends
- Throw from inside a patched path and turn a slow query into a hard 500
Publish from your client facade
typescript
Keep the facade as the only place that talks to the pool. Feature code calls // lib/db.ts
import dc from "node:diagnostics_channel";
const chQuery = dc.channel("acme:db:query");
const chError = dc.channel("acme:db:error");
export async function query<T>(sql: string, params: unknown[] = []) {
const start = performance.now();
try {
const rows = await pool.query(sql, params);
if (chQuery.hasSubscribers) {
chQuery.publish({
sql: sql.slice(0, 80), // ✅ bound size
ms: performance.now() - start,
rows: rows.rowCount,
});
}
return rows as T;
} catch (err) {
if (chError.hasSubscribers) chError.publish({ sql: sql.slice(0, 80), err });
throw err;
}
}
query(), never pool.query() directly — otherwise you reintroduce uninstrumented paths.
Subscribe once at boot
typescript
Import instrumentation before the app listens. For undici, subscribe to // instrumentation/db-metrics.ts
import dc from "node:diagnostics_channel";
dc.subscribe("acme:db:query", (msg: any) => {
// ✅ sync, allocation-light
metrics.histogram("db.query.ms", msg.ms, { bound: msg.sql });
});
dc.subscribe("acme:db:error", (msg: any) => {
metrics.incr("db.query.errors");
logger.warn("db_error", { sql: msg.sql, err: String(msg.err) });
});
undici:request:create / undici:request:headers rather than patching fetch — complements undici dispatcher pools.
Built-in channels worth wiring first
| Channel family | What you get | Typical metric |
|---|---|---|
undici:request:* |
Outbound HTTP timing | http.client.ms |
acme:db:query |
SQL latency + row counts | db.query.ms |
acme:cache:* |
Hit/miss + serialize cost | cache.op.ms |
acme:agent:tool |
Coding-agent tool calls | agent.tool.ms |
docs/channels.md) with SemVer rules: additive fields OK; renaming a channel is a major bump.
Tracing bridge without OpenTelemetry monkey patches
typescript
This keeps OTEL optional: if the bridge file is not imported in a given environment, channels still work for Prometheus-only setups. See OpenTelemetry for LLMs for span attribute conventions.
// instrumentation/otel-bridge.ts
import dc from "node:diagnostics_channel";
import { trace, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("acme-db");
dc.subscribe("acme:db:query", (msg: any) => {
const span = tracer.startSpan("db.query");
span.setAttribute("db.statement.preview", msg.sql);
span.setAttribute("db.duration_ms", msg.ms);
span.end();
});
dc.subscribe("acme:db:error", (msg: any) => {
const span = tracer.startSpan("db.query.error");
span.setStatus({ code: SpanStatusCode.ERROR, message: String(msg.err) });
span.end();
});
Test hygiene: subscribe and unsubscribe
typescript
❌ Subscribing inside each request (leaks listeners across tests and warm Lambdas). ✅ Boot-time subscriptions with explicit unsubscribe in tests.
import dc from "node:diagnostics_channel";
import { afterEach, test } from "node:test";
const seen: unknown[] = [];
function onQuery(msg: unknown) {
seen.push(msg);
}
test("query publishes timing", async () => {
dc.subscribe("acme:db:query", onQuery);
await query("select 1");
assert.equal(seen.length, 1);
});
afterEach(() => {
dc.unsubscribe("acme:db:query", onQuery);
seen.length = 0;
});
Rules that keep this safe
| Rule | Why |
|---|---|
Check hasSubscribers |
Zero cost when disabled |
| Sync subscribers only | Async work → separate queue |
| Bound payload size | No full SQL/PII bodies |
| Stable channel names | SemVer your channel catalog |
| No throws from subscribers | Errors must not break queries |
await inside the subscribe callback.
Rolling out without a big bang
- Add facade publish behind
hasSubscribers(no observers yet) — ship - Add metrics subscribers in staging; compare cardinality vs patched baseline
- Flip production feature flag that imports
instrumentation/* - Delete monkey patches only after two weeks of matching dashboards
Closing checklist
✅ Dos – ✅ Facade publish + boot subscribe – ✅hasSubscribers guards
– ✅ Bound, PII-safe payloads
– ✅ Prefer channels over monkey patches
– ✅ Document channel catalog next to metrics
– ✅ Unsubscribe in unit tests
– ✅ Bridge to OTEL optionally, not mandatorily
❌ Don’ts
– ❌ Don’t patch http/https globals in prod
– ❌ Don’t do I/O inside subscribers
– ❌ Don’t publish unbounded rowsets
– ❌ Don’t forget to unsubscribe in unit tests
– ❌ Don’t invent a new channel name per microservice clone
Related reading
- Node.js Event Loop Lag p99
- OpenTelemetry for LLMs
- Lambda Powertools Node Structured Logs
- Deterministic Replay: Agent Sessions
- Node undici Dispatcher Pools
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- Css Animation Performance: Performance-Optimized Motion for Modern UIs
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
