Repository navigation
Conversation
bengl
marked this pull request as ready for review
November 2, 2026 21:54
AbhiPrasad
approved these changes
Nov 3, 2026
AbhiPrasad
left a comment
Member
There was a problem hiding this comment.
I'd clean up some of the code comments, but otherwise lgtm
Member
Author
|
@AbhiPrasad how's this for a comment cleanup? |
Node rethrows an error from a diagnostics_channel subscriber or a timer as an uncaught exception, and an error from an event listener comes out of emit() and skips the listeners after it. Previously, any bug in a microtel handler could take down the app it was tracing, or keep the app's own listeners from running. Now every function microtel hands to Node is wrapped once, when it is subscribed or attached, and reports its errors as an OTelInstrumentationWarning. The warnings are throttled the way export failures are, one per 30 seconds with a running count, and each carries the original stack. The request it happened on carries on untraced or partly traced. Each export handler settles its export before anything that can throw, and the export deadline settles it too, so a failure a guard catches never leaves an awaited flush() waiting. Signed-off-by: Bryan English <bryan@bryanenglish.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Node rethrows an error from a diagnostics_channel subscriber or a timer as an uncaught exception, and an error from an event listener comes out of
emit()and skips the listeners after it. So a bug in microtel could take down the app it traces, or keep the app's own'end'listener from running and leave its request hanging.What changed.
guard(name, fn)inlib/guard.jswraps every function microtel hands to Node:'close', client'end'and'close'beforeExit'end'and'error'listeners, the deadline timer, and the request's'error'listenerA caught error becomes an
OTelInstrumentationWarning, throttled the wayOTelExportWarningis: one per 30 seconds, with a running count. The warning's detail carries the original stack, since that's what fixing the bug takes. The request it happened on carries on untraced or partly traced. microtel doesn't try to repair its own state.Exports always settle. Each export handler settles its export before anything that can throw, and the export deadline settles it too. So a failure a guard catches never leaves an awaited
flush()waiting.Cost. Each wrapper is made once, when it's subscribed or attached, so a guarded call allocates nothing. Its extra call still costs something. CPU profiles of 50k traced HTTP requests, 8 alternating rounds against main:
That's about 120 ns per traced request, or half a percent of the process's CPU. A microbenchmark puts a guarded diagnostics_channel call at 2.9 ns more than a plain one. In the real path the one shared wrapper can't inline the many handlers it calls, so it costs more. A wrapper per call shape, calling handlers directly instead of through
apply, measured the same 6.2 ms.Raw samples
Wrapper time, ms: 5.3, 8.2, 5.4, 6.1, 7.2, 5.0, 6.4, 7.2
microtel's own time without the wrappers, ms:
A throughput A/B of the node:bench suite, 5 interleaved rounds, was within this machine's noise: traced HTTP moved −3.4% and traced fetch +3.9%, while the untraced controls, which never load microtel, moved +0.2% and −4.1%.
Not taken.
try/catchinside each handler instead of a wrapper. It would avoid the wrapper's cost, at the price of the same few lines in every handler.flush(). A direct call throws to its caller, the way Node's own APIs do.Tests. Two child-process fixtures make microtel's own code throw:
instrumentation-failure-child.jspatches span methods and poisons spans. It makes HTTP/1, HTTP/2, and fetch requests, including aborted ones, and drives the timer, buffer-full, andbeforeExitflushes. Every request must complete, nothing may reachuncaughtException, and the last warning's running count must equal the number of failures injected.export-failure-child.jsreplaceshttp.requestwith a fake, so each export guard fires on demand, each in a fresh warning window, and every export'sflush()must still resolve.Removing any single guard, breaking the throttle either way, dropping the stack, or settling an export after a step that can throw fails a test. That's 32 mutations in all.