feat: collector route, dual onRequestError, serveHealth, stderr banner - #58
Merged
Merged
Conversation
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.
Follow-ups collected from migrating 8 consumer repos to 1.0.0. Version bumped 1.0.0 -> 1.1.0 (additive + bug fixes), so merging publishes 1.1.0 - intended; consumers on
^1.0.0pick it up on the Friday dependabot train.1. P1 bug: the tracer banner was on stdout
src/internal/tracer.tsprintedotel: tracing enabled ...viaconsole.logwhenever an OTLP endpoint was set - i.e. production only. stdout is the JSON-RPC channel of a stdio MCP server, so the line corrupts the stream.Banner is now a plain
process.stderr.write(not the log singleton - it is not safely constructible during tracer init, and the service has not loaded its env yet).stdout audit of the whole
src/turned up a second leak:configureDiagnosticsinstalled the SDK'sDiagConsoleLogger, whoseinfo/debuglevels go through the console methods Node routes to stdout.OTEL_LOG_LEVEL=infoon a stdio MCP server would have corrupted the stream just as badly. Replaced with a stderr-only diag logger at every level.console.warn/console.errorwere verified to be stderr in Node and are still allowed;src/browser.tsonly mentionsconsole.errorin prose (browser-side, not a process stream). No other writer found.Tests:
test/contract/stdout-silence.test.tsscans everysrc/**/*.tsraw source (comments included, like the edge-safety test) forconsole.log|info|debug|dir|table|count|group|time,process.stdoutandDiagConsoleLogger;npm run smoke:stdio(new, inverify) spawns a child under the built bootstrap withOTEL_EXPORTER_OTLP_ENDPOINT+OTEL_LOG_LEVEL=infoset and asserts its stdout is byte-for-byte the one JSON-RPC frame the child itself wrote - and that the banner is still on stderr.2. New
collectorRoute(opts?)from/nextApp Router
POSThandler (fetchRequest->Response) for the client-error collector:text/plainsendBeacon bodies, whitelist pinned toparseClientEvents(so the 32-hex non-zerotrace_idrule, the 200-char field truncation and the 4000-char stack cap are shared, not re-derived), same-origin guard, 64KB cap, 20 events/request, a 60/60s in-memory sliding window,no-store+x-robots-tag: noindexon every answer,204empty on success,405+Allowotherwise. Each event logged through the kit log withsource: 'client'.The export is the union of those guards: exact-origin allowlist (default from
OTEL_CLIENT_ERROR_ORIGINS) or host match against the forwarded host (x-forwarded-hostleftmost, thenHost, then the request URL - behind Traefik theHostheader is the edge's), missingOriginrejected unlessSec-Fetch-Site: same-originorallowMissingOrigin, and the size cap now also checks what was actually read, not only the client'scontent-length.3.
onRequestErroris dual-modeIt was a factory (
onRequestError(log) -> handler) while the v1 README promised a value. A bare re-export type-checked, shipped, and silently swallowed every server render error: Next called the factory, got a function back, discarded it.One logger-like argument (has
.errorand.child) -> bound handler, back-compat. Two or more args, or one non-logger arg -> acts as the handler itself over thelogsingleton.instrumentation.tscollapses toexport { register, onRequestError } from '@agentage/observability/next';. Both paths tested, including the bare-re-export trap and a lone non-logger argument.4. New
serveHealth(port, checks?, opts?)from the rootnode:httpserver answeringGET/HEADon/health+/api/healththrough the existinghealthResponsemachinery; returns{ listening, server, close() }.Kept from the shim: bind all interfaces (the container HEALTHCHECK probes
127.0.0.1, admin reaches it over the overlay), port0for tests,close()on shutdown. Fixed what the shim got wrong: nocontent-typeon the success path, exactreq.url === '/health'(so/health?probe=1and/health/404'd), noHEAD, no/api/health, a bare header-less 404, and nolistenerror handling at all -EADDRINUSEwas an unhandled'error'event that crashed the crawler. It now rejectslistening(pre-caught, so an un-awaited failure never crashes the worker). Crawler-specific coupling (passFacts, Mongo) stays in the crawler. Lives insrc/serve-health.ts, nothealth.ts, which must stay edge-safe.5. Request-log idempotency (coordinator follow-up, admin-api)
The
KIT_REQUEST_LOGmarker is only visible on the middleware itself. admin-api wrapscreateRequestLogin a/health-skip closure, the wrapper hides the symbol fromautoWire's stack scan, and every request was logged twice until they setOBS_REQUEST_LOG=off.Took the zero-surface option rather than the README-only one:
isKitRequestLognow follows a.wrappedproperty chain, soObject.assign(mine, { wrapped: createRequestLog(log) })is seen. No new public export. README documents both routes precisely (marker is visible only when mounted directly; wrap ->.wrappedorOBS_REQUEST_LOG=off). Covered by a new express-patch integration test in admin-api's exact shape.Surface budget
Grows 9 -> 11.
CLAUDE.mdupdated with the justification and a higher bar for a 12th: both additions are runtimes thelisten()auto-wiring provably cannot reach (a Next route handler, a worker with no Express app), and in both cases multiple repos had already built it by hand, divergently.Notes / deviations
/next's static graph now includes pino (via the log singleton needed by direct-modeonRequestErrorand bycollectorRoute). Dashboard, landing and catalog-web already import pino intoinstrumentation.tstoday and build green, so this is the established shape - but admin, which only re-exportsregister, picks it up newly. Flagging in case admin's edge build objects.isLoggerLikerequires both.errorand.child; one existing test double intest/unit/user.test.tshad only.errorand was updated.verifygainedsmoke:stdioandsmoke:distnow assertsserveHealth/collectorRouteexist.npm run verifygreen locally (440 tests, build, dist + bootstrap + stdio smokes).