@inkly/otel
v0.0.0
Published
OpenTelemetry tracing + metrics for inkly, built on the core instrumentation seam. Zero hard runtime deps.
Readme
@inkly/otel
OpenTelemetry tracing + metrics for inkly, built on the core
instrumentation seam. One line wires every action and stream dispatch to a span and a set of
metrics.
import { inkly } from "@inkly/core";
import { trace, metrics } from "@opentelemetry/api";
import { createOtelInstrumentation } from "@inkly/otel";
const app = inkly(contract, {
instrumentation: createOtelInstrumentation({
tracer: trace.getTracer("inkly"),
meter: metrics.getMeter("inkly"),
}),
});Why this design
- No hard dependency on
@opentelemetry/api. You pass in aTracerand/or aMeterthat you obtained from your own OTel setup; this package talks to them through small structural interfaces (OtelTracer,OtelMeter).@opentelemetry/apiis an optional peer dependency. This mirrors the way@inkly/redisaccepts any structurally-compatible client, and keeps inkly's core promise of a lean, dependency-light runtime. (The example passes genuine@opentelemetry/apiobjects and is typechecked in CI, which proves the interfaces line up.) - Passive by construction. It is built on inkly's
instrumentationhook, which cannot change a dispatch — it only observes. Every callback inkly invokes is wrapped intry/catchinside the core, so even a buggy exporter can never break a connection or drop a message. - Tracer and meter are independent. Provide only a
tracerfor traces, only ameterfor metrics, or both. Anything you omit is simply not emitted.
Install
pnpm add @inkly/otel @opentelemetry/apiYou also need an OTel SDK (e.g. @opentelemetry/sdk-node) configured elsewhere in your app to export
the spans and metrics. This package only produces them.
What it emits
Spans (when a tracer is given)
One span per action/stream dispatch:
| Field | Value |
| --- | --- |
| name | inkly.<kind> <name> (e.g. inkly.action sendMessage, inkly.stream ticks) |
| kind | SERVER |
| inkly.kind | "action" or "stream" |
| inkly.name | the contract entry name |
| inkly.peer.id | the peer id (spans only; disable with includePeerId: false) |
| status | OK, or ERROR + recordException(error) when the handler throws |
A stream span stays open for the lifetime of the stream and ends when the stream completes, errors, or is cancelled (unsubscribe / disconnect).
Metrics (when a meter is given)
| Metric | Instrument | Unit | Attributes |
| --- | --- | --- | --- |
| inkly.server.dispatch.duration | Histogram | s | inkly.kind, inkly.name, inkly.ok |
| inkly.server.dispatches | Counter | — | inkly.kind, inkly.name, inkly.ok |
| inkly.server.errors | Counter | — | inkly.kind, inkly.name, inkly.phase |
| inkly.server.active_connections | UpDownCounter | — | — |
| inkly.server.connection.duration | Histogram | s | — |
Durations are recorded in seconds, following the OpenTelemetry semantic conventions for duration histograms.
Cardinality: the peer id is attached only to spans, never to metric attributes. Metric attributes are kept to low-cardinality fields (
kind,name,ok,phase) so they don't explode your time-series database.
Options
createOtelInstrumentation({
tracer, // OtelTracer — omit to disable spans
meter, // OtelMeter — omit to disable metrics
prefix: "inkly", // namespace for span names, metric names, attribute keys
includePeerId: true // put peer.id on spans
});Resume + the active-connections gauge
inkly supports session resume: if a socket drops, the client can reconnect and resume the same
session without re-authorizing. Because a resumed connection re-attaches without a fresh open
(and therefore without a fresh onConnect), this package tracks the set of ids it has actually
incremented and only decrements those. The result: inkly.server.active_connections never goes
negative, and a brief network blip on a resumable session is not counted as a brand-new connection.
The trade-off is that the window during which a session is detached-but-resumable is not counted as
"active". This matches inkly's open/close lifecycle semantics exactly.
Caveats
- This package produces telemetry; it does not configure or start an OTel SDK. Set up exporting (OTLP, Prometheus, etc.) separately.
- Spans are not automatically parented to an incoming HTTP trace context — a WebSocket connection is
long-lived and a dispatch is not an HTTP request. Each dispatch span is a root
SERVERspan. If you need linkage to an upstream trace, propagate it yourself via yourauthorizecontext.
License
LicenseRef-Dazza-1.0. See LICENSE.
