Skip to content

feat(next): support instrumentation.ts hooks#188

Open
mnismt wants to merge 2 commits intoHugoRCD:mainfrom
mnismt:feat/next-instrumentation
Open

feat(next): support instrumentation.ts hooks#188
mnismt wants to merge 2 commits intoHugoRCD:mainfrom
mnismt:feat/next-instrumentation

Conversation

@mnismt
Copy link

@mnismt mnismt commented Mar 15, 2026

🔗 Linked issue

Resolves #187

📚 Description

Following up on #187, I went ahead and dug into the codebase to add support for instrumentation.ts: https://nextjs.org/docs/app/guides/instrumentation

A note on captureOutput: In dev mode, evlog pretty-prints wide events to stdout. When captureOutput: true, those prints get re-captured as new stdout events, so you see everything twice. This is expected but noisy. Here's how I'd recommend using it:

Scenario Recommendation
Dev createEvlog() only. Skip captureOutput - pretty tree output is already readable
Production captureOutput: true + silent: true. evlog stays quiet, capture only catches framework/third-party output
Error-only createInstrumentation() without captureOutput. Just onRequestError() for unhandled errors

For dev, keep using onRequestError() only - catching unhandled errors from SSR/RSC/middleware that withEvlog() can't reach.

What this adds:

createInstrumentation() factory at evlog/next/instrumentation that returns { register, onRequestError } - matching the Next.js instrumentation signature exactly:

  • register(): initializes the logger with drain at startup.
  • onRequestError(): emits structured error events with digest, stack trace, request path/method, and routing context through the global drain (fire-and-forget)

Coexistence with createEvlog():

Both can be used independently or together in the same lib/evlog.ts.

startup                          first request
  │                                    │
  ▼                                    ▼
register()                      createEvlog()
  │                                    │
  ├─ initLogger(drain)          configureHandler()
  ├─ _lockLogger()                     │
  │                              isLoggerLocked()?
  │                                yes → skip re-init
  │
  ▼                                    ▼
  onRequestError ──→ globalDrain    withEvlog ──→ state.options.drain

register() calls initLogger() then _lockLogger(). When createEvlog() later triggers configureHandler(), it checks isLoggerLocked() and skips re-initialization.

Each can have its own drain. instrumentation handles onRequestError events, createEvlog handles per-request wide events from withEvlog().

Edge Runtime:

This was a bit tricky.
Next.js evaluates instrumentation.ts in both Node.js and Edge runtimes. Two things I had to work around:

  1. Static analysis: Next.js statically analyzes imports to determine Edge compatibility. I added patchOutput() use globalThis.process instead of bare process.stdout/process.stderr so the bundler doesn't flag it as Node.js-only
  2. Dynamic imports: I changed the instrumentation.ts re-export file to use await import() gated behind NEXT_RUNTIME === 'nodejs' instead of static re-exports. This prevents Edge from bundling Node.js-only modules (node:async_hooks, node:fs/promises, ...) that come in through the drain and adapters

Without this, you get Module not found: Can't resolve 'node:async_hooks' at build time in Edge routes.

Tests:

12 tests covering: config, stdout/stderr patching, onRequestError fields, drain flow, re-entrancy guard, Edge safety, idempotency, enabled: false, defaults, undefined digest.

Ran the full suite: 694/694 passing, zero regressions.

Docs:

I added a new Instrumentation section to the Next.js docs:

CleanShot 2026-03-15 at 17 37 33

Feel free to tweak the wording or restructure it, happy to adjust.

Examples

The nextjs playground doesn't have any route so I tested with a few examples below:

1. A not found page

CleanShot 2026-03-15 at 17 56 01

2. RSC crashes

Here's a RSC that crashes during SSR - no withEvlog() wrapper, no route handler:

// app/crash/page.tsx
async function getUser() {
  throw new Error('Database connection refused: ECONNREFUSED 127.0.0.1:5432')
}

export default async function CrashPage() {
  const user = await getUser()
  return <div>{user}</div>
}

Without instrumentation, this error only shows up as raw stderr. With onRequestError() evlog captures it as a structured event with route context and digest:

CleanShot 2026-03-15 at 18 11 49

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@vercel
Copy link

vercel bot commented Mar 15, 2026

@mnismt is attempting to deploy a commit to the HRCD Projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 15, 2026

Thank you for following the naming conventions! 🙏

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 15, 2026

Merging this PR will degrade performance by 50.36%

⚡ 1 improved benchmark
❌ 4 regressed benchmarks
✅ 91 untouched benchmarks

⚠️ Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
winston (child.info) 198.9 µs 400.6 µs -50.36%
push 1000 events (no flush) 1.8 ms 2.2 ms -17.27%
winston 591.1 µs 457 µs +29.35%
with traceparent 337.1 µs 475.1 µs -29.04%
pino 332 µs 511.7 µs -35.12%

Comparing mnismt:feat/next-instrumentation (47c9031) with main (2165483)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (dc19ba4) during the generation of this report, so 2165483 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Rename underscore-prefixed function to satisfy camelCase naming convention
rule. Also fix space-before-function-paren in instrumentation.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mnismt
Copy link
Author

mnismt commented Mar 17, 2026

@HugoRCD fixed all CI failures (except CodSpeed)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] Next.js: instrumentation.ts support

1 participant