Skip to content

feat: shared Node telemetry package, extracted from Backbone - #1

Merged
victor-carv merged 12 commits into
mainfrom
feat/otel-node-package
Oct 13, 2026
Merged

victor-carv merged 12 commits into
mainfrom
feat/otel-node-package

Conversation

@victor-carv

Copy link
Copy Markdown
Member

Summary

The first version of @codelittinc/otel-node: one in-process OpenTelemetry SDK for every Carbon Node app. It is extracted from Backbone's packages/telemetry, so Backbone's tested behaviour is the starting point; its app-specific constants became options. Part of codelittinc/carbon-backbone#825 (Phase 6, the shared package).

  • SDK boot: traces, metrics, OTLP logs and Pyroscope profiles, exported only when OTEL_EXPORTER_OTLP_ENDPOINT is set. With no endpoint, no SDK module loads. startTelemetry never throws or rejects, and shutdownTelemetry is bounded.
  • Layer 1 of the two-layer PII scrub: span names and attributes, log bodies and fields, and metric labels are scrubbed before any exporter sees them. The scrub contract (src/scrub-contract.json) now lives here; the agent's layer 2 in carbon-observability follows it.
  • Logging: a pino logger whose scrubbed lines go to OTLP logs. A new captureConsole() sends console.warn and console.error through the same scrub, so an app that logs with console needs no rewrite. Console capture never exports anything log would not.
  • Entries:
    • . is the main API.
    • ./register is for node --import with no options.
    • ./bullmq holds the BullMQ telemetry. bullmq ^6.3.8 is the only optional peer.
    • ./url-credentials is browser-safe: no Node and no OTel imports.
    • ./testing holds the test helpers.
  • Options:
    • serviceName
    • logScope
    • metricViews (instrument prefix plus an allowed-label list)
    • instrumentations: app-supplied, so Prisma matches the app's version
    • redactKeys: adds keys to the default list and never removes one. It also applies to span attribute keys: a whole name anywhere, or a last segment unless the attribute is one of 54 listed attributes the package's instrumentations emit (such as http.route or db.system.name). Anything unlisted fails closed
    • onError
    • ignoredPaths
  • Install by commit: dist/ is committed and marked as generated. CI fails when it differs from a fresh build. There is no license, by decision: the repo is public only so apps can install it without a token.

Changes from Backbone at 6a948f21 that its move (next PR) will declare

  • The tracer scope name is now @codelittinc/otel-node, and Prisma instrumentation registers after pg.
  • PostHog stays in Backbone. Backbone's wrapper flushes it on shutdown and passes logScope: "backbone".
  • log now scrubs values it used to leak: functions, and Maps, Sets, RegExps or Date subclasses with their own toJSON.
  • Error messages that are not strings, and Prisma meta.target items, are scrubbed too.
  • Format placeholders render a few rare values differently (for example, %d with a Date).
  • A nested typed array that is not a Buffer exports { bytes }.

Ordinary values log byte for byte as before.

Testing

  • pnpm typecheck passes.
  • pnpm test: 38 files, 947 tests, all passing. Moved Backbone tests keep their assertions, with Backbone's values passed as options. Two assertion lines changed: one now imports from ./bullmq, and the lockstep check now covers this package's own manifest. The new tests cover:
    • a run with no endpoint
    • an unreachable endpoint, which delays a request by at most 50 ms
    • console capture
    • scope filtering
    • metric views
    • the ./url-credentials import graph
    • typechecking the ./bullmq types without bullmq installed
    • a table of 14 serialization hooks × 6 positions, through both log and console capture
  • pnpm build matches the committed dist/.
  • An install by commit from a local git+file: clone works with no build step: every entry imports, and register boots the SDK.
  • Reviews:
    • a task review with two fix rounds
    • a whole-branch review with one fix wave and a scoped re-review
    • no open Critical or Important findings

Not yet verified: the token-free github: install, which needs this branch on GitHub, and Backbone's own suite against the package, which is the next PR.

🤖 Generated with Claude Code

victor-carv and others added 12 commits October 12, 2026 22:21
pnpm, Node 22, TypeScript built to dist/, vitest, ESM. Dependency pins
match Backbone's packages/telemetry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backbone's SDK boot, two-layer scrub's layer 1, OTLP logger and trace
helpers move from packages/telemetry. Its constants become options:
logScope, metricViews, instrumentations (Prisma leaves the default
list), redactKeys and onError. PostHog error tracking and Backbone's
metric attribute list stay in Backbone. url-credentials has its own
browser-safe entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
captureConsole sends console.warn and console.error lines through the
logger's scrub to OTLP logs under the configured scope, and still
writes them to the console. register starts telemetry under
node --import from OTEL_SERVICE_NAME alone. Tests pin the defaults: no
SDK module without an endpoint, a dead collector costs a request at
most 50 ms, no metricViews keeps every attribute, and the scope filter
keeps the default scope's lines and drops every other scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apps install the repo as a git dependency pinned to a commit, so the
built output ships in the commit. The build drops source maps, whose
sources are not in the installed package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Arguments were sanitized one by one before formatting, so a value that
takes its shape only once formatted (split across %s arguments, a
custom inspect or toString) reached the exporter unscrubbed, and a Map
or Set printed its entries past the key redaction. Collections are now
expanded before the field sanitizer runs, and the formatted message is
scrubbed as the last step, with long digit runs included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI runs the type check, the tests and the dist/ check on pull requests
and pushes to main, read-only and with pinned actions. The README
covers install by commit, the options, the two-layer scrub and the
rules for an app using the package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntry

Console arguments are now reduced to the logger's JSON form before
formatting, so a function's properties and source, and Map, Set or byte
view contents (including inside a toJSON result) are never exported,
and a huge array is read only up to the sanitizer's breadth. A test
pins that the SDK's console diag logger bypasses the capture.

bullmqTelemetry moves to the ./bullmq entry, so the main entry's types
never name bullmq; the optional peer is ^6.3.8, as bullmq-otel needs.
A test typechecks the built main entry without bullmq, skipLibCheck off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sanitizer's output is now inert in both log and console capture. A
function is dropped (its own toJSON never runs), what any toJSON
returns is walked like any value, a Map, Set or RegExp becomes {} and a
byte view { bytes } whatever toJSON or own properties it carries, and
a date is read with the built-in methods. An error's message and
Prisma's meta target are copied only as strings, and a non-string
message never reaches the stack header. A table test covers each hook
at the top level, nested, in an array, Map, Set and toJSON result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A primitive toJSON result (a Decimal's string) costs no depth.
- Console capture puts an Error's safe message where %s takes it.
- redactKeys also redacts span attributes, by whole name or last
  segment; the agent's layer 2 does not know them.
- startTelemetry: the first call's options win whether or not it
  exports, with a telemetry_restart_ignored warning, and options it
  cannot read log telemetry_start_failed instead of rejecting.
- Tests pin the inert format-arg rendering and assert the hooks table
  never passes by failing closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README lists every exported type, says how an app checks an
instrumentation it passes in, and states that layer 2 does not repeat
redactKeys. CI cancels superseded runs on pull requests only, so every
main commit gets a verdict. dist/ is marked generated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… keys

startTelemetry validates and copies its options before it changes any
state, then applies the log scope, error hook and redact keys in one
step, so a failed call leaves nothing half-applied. A redact key that
is not a string is dropped with one invalid_redact_key warning naming
only its type; the valid keys still apply. Unreadable options on a
later call return the running config. An app key matches a span
attribute's last segment only outside OpenTelemetry's namespaces, so
`name` never redacts db.system.name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An app key now matches a span attribute's last segment everywhere
except a fixed list of attributes the instrumentations, BullMQ and
Prisma emit (db.system.name, http.route, bullmq.job.id, ...), so an
attribute the app sets under an OpenTelemetry namespace is still
redacted. Content-bearing attributes stay off the list. Prisma's bare
name, method and model are matched by whole name, as documented.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@victor-carv

Copy link
Copy Markdown
Member Author

QA Check Report — codelittinc/otel-node feat/otel-node-package (000e84b..f56b7d1)

Risk level: HIGH

Base HIGH: 85 files and 7,526 lines, excluding dist/ and the lockfile. Escalations: none.

  • A written plan exists (Backbone tasks/observability-platform/phase-6-otel-node.md, with rulings R184–R212).
  • Every file is new to this repo, so none counts as unhealthy.

Spec discipline

  • Spec or acceptance criteria: yes.
    • The plan's Phase 6 Task 16, amended by controller rulings in the session ledger.
    • The diff matches it.
    • Deviations are recorded as rulings: the API superset, the ./bullmq subpath, no next peer, and keeping the event name telemetry_start_failed.

Complexity (Quality Paradox)

  • Files touched: 85 (36 src, 38 test files, config, CI, README).
    • Most moved byte-identical from Backbone packages/telemetry at 6a948f21. The moved-file diff against the originals is 772 lines.
    • New logic: console-capture.ts, options.ts, register.ts, app-keys.ts, plus scrub hardening in log-redact.ts and log-errors.ts.
  • Rework signals:
    • console-capture.ts, log-redact.ts, sdk.ts and app-keys.ts were each re-touched in 2–5 review fix rounds within this PR. That is in-PR convergence on review findings, not post-merge rework.
    • Each round added a test that failed first.
    • app-keys.ts went through two designs: a namespace exemption, then the exact list of attribute names.
  • Unhealthy files: none. The largest file is 287 lines (a test); all are ≤ 300.
  • Sustainable velocity concern: no. Every new code path has tests: 38 files, 947 tests.

Test integrity

  • No test deleted, skipped or .only'd.
  • Of the moved Backbone tests, two lost an original assertion line, both justified:
    • sdk-lazy.test.ts: the same assertion, now importing bullmqTelemetry from the ./bullmq subpath (R198).
    • otel-lockstep.test.ts: the cross-workspace drift check became a per-family single-version check over this package's own manifest (R187). Backbone keeps its workspace version.
  • Replaced within this PR: round 3's "bad redact key fails the start" test became "a bad key is dropped with a type-only warning; valid keys apply". This follows ruling R209 and replaces a test added earlier in the same PR, not a pre-existing one.
  • New tests assert observable output (exported OTLP records, stdout lines, span attributes), not mocks:
    • serialization-hooks.test.ts: 14 hooks × 6 positions, through both log and console capture. It also asserts log_redaction_failed is absent, so a fail-closed sanitizer can't pass it.

Dependency provenance

  • Dependency files changed. Run /supply-chain-check before merging. package.json and pnpm-lock.yaml are new in this repo.
  • All 22 runtime dependencies and the bullmq dev dependency are pinned exactly as Backbone pins them at 6a948f21. Their provenance is unchanged.
  • New dev-only dependencies:
    • vitest@3.2.7: exists, package created 2021, a well-known maintainer org.
    • @types/node@22.19.17: exists, the DefinitelyTyped scope.
    • typescript moved from a range to the exact 5.9.3.
  • No name proximity concerns. Peers: bullmq ^6.3.8, optional (matches bullmq-otel@2.0.1).

Validation gaps (Quality Paradox)

  • startTelemetry deliberately never throws or rejects:

    • Failures log telemetry_start_failed.
    • Bad redact keys log invalid_redact_key, carrying the entry's type only.
    • A restart with different options logs telemetry_restart_ignored.

    This is intended and declared: telemetry must not take down the app. It is tested.

  • Not verified in this PR:

    • the token-free github: install, which needs the branch on GitHub (verify after push)
    • Backbone's own suite against the package (P6-T17)
    • CI on GitHub (first run after push)

AI-specific concerns (Quality Paradox)

  • Hallucinated APIs: none found.
    • CI action SHAs were checked against their tags (checkout v7.0.1, action-setup v6.1.0, setup-node v7.1.0).
    • The @prisma/instrumentation 7.10.0 attribute list was checked against the published tarballs.
  • Duplicated logic: url-credentials.ts has one copy here. Backbone's web copy is removed in P6-T17 (R184).
  • Silent failures: catch blocks in console-capture (the console line is already written) and in shutdown are intentional and commented. No 200-on-failure paths.
  • Slop: none significant. Comments state end results; no dates or internal ids.

Contract compliance

  • The public API is typed. The README lists every export and type.
  • The scrub contract src/scrub-contract.json is pinned by scrub-contract.test.ts. Its update hint names carbon-observability as the follower; that platform PR (P6-T16c) is pending (R191).

Architecture violations

  • None blocking.
  • Process-global state (started config, applied options, app keys) is module-level by design: one SDK per process. Each piece has a test seam.
  • Adapter boundary: OTel and pino sit behind the package's own API, and apps import only @codelittinc/otel-node.

Recommended actions

  1. After push: confirm CI is green and that a github:codelittinc/otel-node#<sha> install works with no token.
  2. Run /supply-chain-check on the new lockfile.
  3. Land the platform contract PR (P6-T16c) before P6-T17 deletes Backbone's copy.

@victor-carv
victor-carv marked this pull request as ready for review October 13, 2026 02:18
@victor-carv
victor-carv merged commit 134dcf5 into main Oct 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant