@m4ike1/ion-telemetry
v0.1.1
Published
Vendor-neutral telemetry contracts and typed schema utilities for ion
Maintainers
Readme
@m4ike1/ion-telemetry
Vendor-neutral telemetry contracts and typed schema utilities for ion.
This package defines how ion code records telemetry without depending on a backend. It provides:
- an explicit, callback-based
TelemetryContext/TelemetrySpancontract; - W3C
traceparentpropagation across process and service boundaries; - a shared
NOOP_TELEMETRY_CONTEXT; - a reference
InMemoryTelemetryContextimplementation with injectable timing; - serializable schema definitions with inferred TypeScript types and sensitive-attribute enforcement;
- an adapter conformance suite under
@m4ike1/ion-telemetry/testing.
It provides no exporter, no ambient current-span state, and no backend dependency. Applications pass a context explicitly and bridge it to OpenTelemetry, Sentry, logs, or another backend with an adapter.
Installation
npm install @m4ike1/ion-telemetryRequires Node >=22.19. The root entrypoint is runtime-neutral (no Node
APIs); only the /testing subpath uses node:assert/strict.
Quick start
Accept a context parameter defaulting to no-op, and start a span around the work:
import {
NOOP_TELEMETRY_CONTEXT,
type TelemetryContext,
} from '@m4ike1/ion-telemetry';
async function loadAccount(
accountId: string,
telemetryContext: TelemetryContext = NOOP_TELEMETRY_CONTEXT,
) {
return telemetryContext.startSpan(
{
name: 'example.account.load',
attributes: { 'example.account.id': accountId },
},
async (span) => {
const account = await readAccount(accountId);
span.setAttributes({ 'example.account.found': account !== undefined });
return account;
},
);
}Nest by starting child spans from the callback span:
return telemetryContext.startSpan({ name: 'example.parent' }, async (parentSpan) => {
return parentSpan.startSpan({ name: 'example.child' }, async (childSpan) => {
childSpan.addEvent('example.cache.lookup', { 'example.cache.hit': true });
return performWork();
});
});startSpan() returns a synchronous callback result directly and returns a
promise only when the callback returns a promise or throws. Existing await
call sites work for both forms.
Propagate a W3C traceparent when work crosses an RPC, subagent, lane, or service boundary:
const outgoing = await telemetry.startSpan({ name: 'example.parent' }, async (span) => {
const headers = { traceparent: telemetry.inject(span) };
await callRemoteService(headers);
});
const remoteTelemetry = telemetry.extract(incomingHeaders.traceparent);
await remoteTelemetry.startSpan({ name: 'example.remote' }, runRemoteWork);extract() never throws for malformed input: an invalid or missing
traceparent produces a new root trace. Future traceparent versions with a
valid W3C prefix are accepted and emitted as version 00; only the sampled
flag is propagated. inject() returns undefined for a span the context does
not own.
Capture spans in tests with the in-memory reference adapter:
import { InMemoryTelemetryContext } from '@m4ike1/ion-telemetry';
const telemetry = new InMemoryTelemetryContext();
await loadAccount('123', telemetry);
console.log(telemetry.getSpans());Constrain names and attributes at compile time with a typed schema:
import {
createTypedSpanStarter,
defineTelemetrySchema,
} from '@m4ike1/ion-telemetry';
const schema = defineTelemetrySchema({
version: 1,
spans: {
'example.read': {
description: 'Read one resource',
parents: { kind: 'any' },
startAttributes: {
'example.resource': { type: 'string', required: true, description: 'Resource kind' },
},
endAttributes: {
'example.item_count': { type: 'number', description: 'Items returned' },
},
status: { default: 'ok', errorWhen: 'The read throws' },
},
},
} as const);
const startSpan = createTypedSpanStarter(telemetryContext, [schema]);
await startSpan('example.read', { 'example.resource': 'account' }, async (span) => {
const accounts = await readAccounts();
span.setAttributes({ 'example.item_count': accounts.length });
return accounts;
});The typed starter also supplies the matching span definition to the adapter at
runtime. Attributes declared sensitive: true must be omitted, hashed, or
truncated before recording. The in-memory adapter uses the shared
redactAttributes() helper from @m4ike1/ion-telemetry, which omits those
values from start, end, and event attributes.
Docs
docs/concepts.md— span model, explicit propagation, settlement semantics.docs/adapters.md— implement a backend adapter, run the conformance suite, test with the in-memory adapter.docs/schemas.md— define schemas and use typed span starters.docs/reference.md— every export with signatures.
Development
From this package directory:
npm test
npm run buildRepository-wide checks run with npm run check from the repo root.
License
MIT
