@defjs/opentelemetry-server 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,31 +1,20 @@
1
1
  # @defjs/opentelemetry-server
2
2
 
3
- Server-side outbound OpenTelemetry integration for `@defjs/core` HTTP, SSE, and WebSocket clients. It is a middleware plugin: the plugin form of an interceptor chain.
3
+ Server-side outbound OpenTelemetry for `@defjs/core` HTTP, SSE, and WebSocket clients.
4
4
 
5
- Instrument a consuming application's client while keeping its imported SDK definitions independent of telemetry configuration. The application owns providers, exporters, and shutdown; the API SDK continues to export reusable operations. References to the OpenTelemetry SDK below mean the telemetry runtime, not the API SDK built with Defjs.
5
+ It is a middleware plugin — the plugin form of an interceptor chain. Your application keeps its OpenTelemetry SDK, providers, exporters, and shutdown; the imported SDK definitions stay free of telemetry configuration. This package is outbound client instrumentation for server applications, not inbound server instrumentation.
6
6
 
7
- **Core Positioning**:
8
-
9
- - **Outbound-only client instrumentation** — This package wraps defjs clients that make outbound requests. It is not inbound server instrumentation.
10
- - **Server environment** — Designed for server-side applications with an OpenTelemetry SDK, not browser-only telemetry setup.
11
- - **Does not initialize the SDK** — Configure your application's providers, then pass optional `tracer`, `meter`, and `logger` instances into `withOpenTelemetryServer(...)`. The signals work independently. It never obtains a tracer or logger implicitly. API objects without a configured provider are silent no-ops.
12
- - **Per-transport separation** — HTTP, SSE, and WebSocket each have independent interceptors, span lifecycles, and metric dimensions.
13
-
14
- ## Installation
15
-
16
- Install `@defjs/opentelemetry-server` with the matching `@defjs/core` release line and its OpenTelemetry peers:
7
+ ## Install
17
8
 
18
9
  ```sh
19
10
  bun add @defjs/core @defjs/opentelemetry-server @opentelemetry/api @opentelemetry/core
20
11
  ```
21
12
 
22
- The package is ESM and requires `@defjs/core` as a peer in the `^0.4.0` range. Its tarball retains this `README.md` and the repository `LICENSE`; repository-wide guides and examples remain outside the package.
13
+ The package is ESM and requires `@defjs/core` as a peer in the `^0.5.0` range.
23
14
 
24
- The native Logs API (`@opentelemetry/api-logs`) is a regular dependency, so existing consumers do not need another peer. Install SDKs, exporters, and third-party logging bridges in your application when needed. The package consumes the optional tracer, meter, and logger you pass; it does not emit spans without a tracer or logs without a logger.
15
+ ## Quick start
25
16
 
26
- ## Basic Usage
27
-
28
- For tracing, this package does not install a `TracerProvider`. `trace.getTracer('my-service')` without `trace.setGlobalTracerProvider(...)` (your SDK's `start()` usually does this) returns a no-op tracer: interceptors still wrap requests, but spans are silently discarded. Pass a recording tracer from your SDK, or register the provider first:
17
+ It never initializes a provider or looks one up implicitly: pass the `tracer`, `meter`, and `logger` you want, and each signal works independently. Without a `tracer` no spans are created — interceptors still wrap requests, but `trace.getTracer()` returns a no-op until your SDK registers a provider.
29
18
 
30
19
  ```typescript
31
20
  import { createClient, withEndpoint } from "@defjs/core";
@@ -37,353 +26,78 @@ const tracer = trace.getTracer("my-service");
37
26
 
38
27
  const client = createClient(
39
28
  withEndpoint("https://api.example.com"),
40
- withOpenTelemetryServer({
41
- tracer,
42
- }),
29
+ withOpenTelemetryServer({ tracer }),
43
30
  );
44
31
  ```
45
32
 
46
- ### Metrics without local spans
47
-
48
- Omit `tracer` to record metrics without local spans. The plugin never calls `trace.getTracer(...)` and does not invoke `startSpanHook`, `requestHook`, or `responseHook` in this mode. Initialize your application's OpenTelemetry SDK with a `MeterProvider` separately, then pass its meter. Best practice: name the command and return from the function when the error-first tuple is set:
33
+ Omit `tracer` to record metrics only; the plugin then never calls `trace.getTracer(...)` or the span hooks. `logger` accepts the native `Logger` from `@opentelemetry/api-logs`, not a Winston or Pino business logger.
49
34
 
50
35
  ```typescript
51
- import { createClient, defineRequest, withEndpoint } from "@defjs/core";
52
- import { withOpenTelemetryServer } from "@defjs/opentelemetry-server";
53
36
  import { metrics } from "@opentelemetry/api";
54
37
 
55
38
  const meter = metrics.getMeter("my-service");
56
- const readOrders = defineRequest({
57
- method: "GET",
58
- operation: "orders.read",
59
- path: "/orders",
60
- });
61
39
 
62
40
  const client = createClient(
63
41
  withEndpoint("https://api.example.com"),
64
42
  withOpenTelemetryServer({ meter }),
65
43
  );
66
-
67
- async function main() {
68
- const command = readOrders();
69
- const [error] = await client.execute(command);
70
- if (error) {
71
- console.error(error.code);
72
- return;
73
- }
74
- }
75
-
76
- void main();
77
- ```
78
-
79
- Context propagation still forwards the extracted parent context without a local span; WebSocket `queryPropagation` still defaults to `false`. With a `tracer`, `requireParentSpan: true` bypasses spans, propagation, hooks, metrics, and logs when no active parent exists. Without a `tracer`, it blocks neither metrics nor logs.
80
-
81
- ### Logs and third-party loggers
82
-
83
- `logger` accepts the native `Logger` from `@opentelemetry/api-logs`. Pass one from your application's provider; a Winston or Pino business logger is a different interface. Logger-only usage does not create local spans:
84
-
85
- ```typescript
86
- import { createClient, withEndpoint } from "@defjs/core";
87
- import { withOpenTelemetryServer } from "@defjs/opentelemetry-server";
88
- import {
89
- LoggerProvider,
90
- SimpleLogRecordProcessor,
91
- ConsoleLogRecordExporter,
92
- } from "@opentelemetry/sdk-logs";
93
-
94
- const provider = new LoggerProvider({
95
- processors: [
96
- new SimpleLogRecordProcessor({ exporter: new ConsoleLogRecordExporter() }),
97
- ],
98
- });
99
- const client = createClient(
100
- withEndpoint("https://api.example.com"),
101
- withOpenTelemetryServer({
102
- logger: provider.getLogger("@defjs/opentelemetry-server"),
103
- }),
104
- );
105
- // Use client, finish pending requests and streams, then await provider.shutdown().
106
44
  ```
107
45
 
108
- Defjs emits at most one diagnostic per request or logical connection, always `WARN` / 13:
109
-
110
- | Outcome | Event / phase |
111
- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
112
- | HTTP `next()` rejects, excluding cancellation and public HTTP_STATUS faults | `http.client.request.exception` / `request` |
113
- | SSE or WebSocket connection fails | `defjs.client.sse.exception` or `defjs.client.websocket.exception` / `connect` |
114
- | SSE or WebSocket ends in error, including a rejected `closed` | Same transport event / `connection` |
115
-
116
- Successful responses, HTTP status responses (including 4xx/5xx whose declared `error` body decodes), normal closure, EOF, and caller cancellation produce no LogRecord. An HTTP execute timeout logs once only when this interceptor observes its `next()` reject in the chain / transport. An abort whose reason is a `TimeoutError` (for example `AbortSignal.timeout()`) is classified as a timeout, not a cancellation, but the same observation boundary applies. Timeouts during input parsing, the builder, interceptor work after `await next()`, or declared-body conversion after the chain need outer command-level observation; they do not necessarily produce a transport LogRecord.
117
-
118
- Core reads a declared `output` (2xx) or `error` (other status) body inside the HTTP interceptor chain. A read failure in any representation makes `next()` reject with an internal `BodyFailure`. Only JSON representation checks for a JSON media type and JSON-parses the body; a failure there also rejects with `BodyFailure`. These rejections emit one HTTP record whatever the status, and the command returns `NET_BODY_INCOMPLETE`, `RES_MEDIA_TYPE_INVALID`, or `RES_DECODE_FAILED`. Text, blob, and bytes representations accept normal non-JSON bodies without logging. A body that is not declared is never read and cannot log. HTTP Struct validation (`RES_STRUCT_MISMATCH`), an absent declared body such as a 204, and the later HTTP_STATUS conversion happen after the interceptor and do not log.
119
-
120
- An SSE non-2xx handshake is observed as a connection failure and logs once. If its declared `error` body decodes, Core returns `HTTP_STATUS`; if that body fails its Struct, Core returns `RES_DECODE_FAILED` with a `StructError` cause, not `RES_STRUCT_MISMATCH`. A WebSocket close frame from the server (any code, for example 1000 or 1011) resolves as a normal close and produces no LogRecord. An abrupt drop without a close frame (code 1006) depends on the runtime: where the platform WebSocket reports a socket `error` before `close` (Node.js), the session ends in error and logs one WARN with `websocket.close.code` `1006`; where it does not (Bun), the drop resolves as a normal close and nothing is logged. Otherwise only a session that ends in error, such as an incoming queue overflow or a heartbeat timeout, logs. Command input validation, individual messages, reconnect attempts, and hook failures do not produce extra Logs. The HTTP exception convention is currently Development; SSE/WS names are Defjs-specific.
121
-
122
- `exception.type` is the class name of what the interceptor observed, not a Defjs fault code. It can be an internal class (`BodyFailure` for an HTTP body, `EventStreamFatalError` for an SSE handshake, `DefjsFault` for a WebSocket startup failure) or a platform class such as `TypeError` for a network failure and `Error` for a timeout.
46
+ ## Operation identity
123
47
 
124
- Diagnostics contain a fixed message, exception type, phase, static operation, available server address/port, and protocol fields. They do not copy raw URLs, query strings, headers, bodies, baggage, close reasons, error messages/stacks, or hook attributes. Keep custom operation and exception names non-sensitive. Each record explicitly uses the saved outbound context, including asynchronous closure; span sampling does not suppress these logs. Logging failures cannot replace the business result.
125
-
126
- For third-party business logging, connect the library to the same application's Logs pipeline:
127
-
128
- - **Winston:** register the Logs provider, create `OpenTelemetryTransportV3`, and inject a native Logger into Defjs. The transport resolves the global provider lazily, so registration only has to happen before the first record is logged; earlier records are dropped. Use the explicit transport or automatic log sending once, to avoid duplicates.
129
- - **Pino on Node.js:** start the SDK with `PinoInstrumentation` before loading Pino. Log correlation and log sending are separate features. A Pino worker transport can have a separate provider.
130
- - **Levels:** OTel TRACE/DEBUG/INFO/WARN/ERROR/FATAL use base values 1/5/9/13/17/21. Bridge mappings convert the library's levels; its numeric levels are not OTel severity numbers. A Winston transport threshold does not filter Defjs's direct Logger; use a shared processor or Collector for a common policy.
131
-
132
- See the [complete integration guide](https://github.com/defjs/defjs/blob/main/doc/content/en-US/docs/integrations/opentelemetry-server.mdx#logs-and-third-party-loggers) for mappings and Pino initialization order. The [runnable Winston example](https://github.com/defjs/defjs/tree/main/doc/examples/opentelemetry-logging) uses a real Logs SDK, demonstrates DEBUG console output versus INFO OTel output, and closes its resources. In this repository, run `bun doc/examples/opentelemetry-logging/winston.ts`.
133
-
134
- ## Operation Identity
135
-
136
- Set the optional `operation` field statically on `defineRequest(...)`, `defineEventStream(...)`, or `defineWebSocket(...)`:
48
+ Set `operation` statically on the definition. It is the low-cardinality transport identity: spans are named `GET orders.read`, `SSE orders.watch`, `WebSocket orders.connect`, and `defjs.operation` is recorded on spans and metrics.
137
49
 
138
50
  ```typescript
139
51
  const readOrder = defineRequest({
140
52
  method: "GET",
141
53
  operation: "orders.read",
142
54
  path: "/orders/:id",
143
- // input and output omitted
144
55
  });
145
56
  ```
146
57
 
147
- An explicit operation is the low-cardinality transport identity. HTTP spans use names such as `GET orders.read`; SSE and WebSocket spans use `SSE orders.watch` and `WebSocket orders.connect`. Spans and metrics also receive `defjs.operation`.
148
-
149
- Without `operation`, the previous fallback remains unchanged: HTTP uses the request method as its span name, SSE uses `SSE`, WebSocket uses `WebSocket`, and `defjs.operation` is omitted. Never infer an operation from `req.endpoint`, a resolved URL, or a path containing identifiers. Do not copy resolved URLs into custom attributes or logs.
58
+ Without it, HTTP spans use the method alone and SSE/WebSocket use their transport name. Never derive an operation from a resolved URL or a path containing identifiers.
150
59
 
151
- ## Full Configuration
60
+ ## Configuration
152
61
 
153
62
  ```typescript
154
- const client = createClient(
155
- withEndpoint("https://api.example.com"),
156
- withOpenTelemetryServer({
157
- tracer, // Optional, local spans and span hooks only run when provided
158
- meter, // Optional, metrics only collected when provided
159
- logger, // Optional native OTel Logger, failure diagnostics only
160
- propagator, // Optional, default W3C TraceContext + Baggage
161
- requireParentSpan: false,
162
- http: {
163
- enabled: true,
164
- startSpanHook(req) {
165
- return { "app.operation": req.operation ?? "unclassified" };
166
- },
167
- responseHook(span, res, req) {
168
- span.setAttribute("defjs.response.status_text", res.statusText);
169
- },
170
- },
171
- sse: {
172
- enabled: true,
173
- },
174
- webSocket: {
175
- enabled: true,
176
- queryPropagation: false, // Safe default; set true only after reviewing URL propagation
177
- },
178
- }),
179
- );
180
- ```
181
-
182
- ## WebSocket propagation safety
183
-
184
- `webSocket.queryPropagation` defaults to `false`. Omitting it leaves the WebSocket query string unchanged and does not inject trace context or baggage into the URL.
185
-
186
- Browser WebSocket clients usually cannot set arbitrary headers. If you have reviewed URL-based propagation for your environment and accept its exposure boundary, enable it explicitly:
187
-
188
- ```ts
189
- withOpenTelemetryServer({
190
- tracer,
191
- webSocket: {
192
- queryPropagation: true,
193
- },
194
- });
195
- ```
196
-
197
- Injected fields are written to the end of the query string and replace any parameter of the same name. All other parameters keep their serialized form, including a propagation parameter the propagator has nothing to inject for.
198
-
199
- Query strings can be recorded by proxies, browsers, APM tooling, access logs, and network debugging tools. They may also contain tokens or other high-cardinality user input. If your propagator includes `baggage`, baggage values can also be written into the URL and may contain sensitive data.
200
-
201
- With the safe default, trace context does not ride on the WebSocket URL. Use another reviewed correlation mechanism at the application layer, such as a protocol-reviewed first frame or a short-lived connection ticket, if your server still needs to link the connection to a trace.
202
-
203
- ### Configuration Options
204
-
205
- | Option | Type | Default | Description |
206
- | ------------------- | --------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
207
- | `tracer` | `Tracer` | `undefined` | External OpenTelemetry tracer; omitting disables local spans and span hooks |
208
- | `meter` | `Meter` | `undefined` | External OpenTelemetry meter, omitting disables metrics |
209
- | `logger` | `Logger` from `@opentelemetry/api-logs` | `undefined` | External native Logger; omitting disables diagnostic LogRecords |
210
- | `propagator` | `TextMapPropagator` | W3C TraceContext + Baggage | Custom context propagator |
211
- | `requireParentSpan` | `boolean` | `false` | With a tracer, bypass all instrumentation when no active parent exists; does not block logger/meter-only use |
212
- | `http` | `OpenTelemetryServerHttpOptions` | `{}` | HTTP transport trace/metric options |
213
- | `sse` | `OpenTelemetryServerSSEOptions` | `{}` | SSE transport trace/metric options |
214
- | `webSocket` | `OpenTelemetryServerWebSocketOptions` | `{}` | WebSocket transport trace/metric options |
215
-
216
- ### HTTP Options
217
-
218
- | Option | Type | Default | Description |
219
- | --------------- | ------------------------------------------- | ----------- | ------------------------------------------------------------------------------ |
220
- | `enabled` | `boolean` | `true` | Enable HTTP instrumentation |
221
- | `startSpanHook` | `(req) => Attributes` | `undefined` | Add or override synchronous initial attributes before the HTTP span is created |
222
- | `requestHook` | `(span, req) => void \| Promise<void>` | `undefined` | Customize HTTP span before request, `req` is `HttpRequest` |
223
- | `responseHook` | `(span, res, req) => void \| Promise<void>` | `undefined` | Customize HTTP span after response; `req` is the original `HttpRequest` |
224
-
225
- ### SSE Options
226
-
227
- | Option | Type | Default | Description |
228
- | --------------- | ---------------------------------------------- | ----------- | ------------------------------------------------------------------------------------ |
229
- | `enabled` | `boolean` | `true` | Enable SSE instrumentation |
230
- | `startSpanHook` | `(req) => Attributes` | `undefined` | Add or override synchronous initial attributes before the SSE span is created |
231
- | `requestHook` | `(span, req) => void \| Promise<void>` | `undefined` | Customize SSE span before stream request |
232
- | `responseHook` | `(span, stream, req) => void \| Promise<void>` | `undefined` | Customize SSE span after stream handle returned; `req` is the original `HttpRequest` |
233
-
234
- ### WebSocket Options
235
-
236
- | Option | Type | Default | Description |
237
- | ------------------ | ----------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------- |
238
- | `enabled` | `boolean` | `true` | Enable WebSocket instrumentation |
239
- | `queryPropagation` | `boolean` | `false` | Inject trace context and baggage into the WebSocket URL query string. Enable only after reviewing URL logging and disclosure risks. |
240
- | `startSpanHook` | `(req) => Attributes` | `undefined` | Add or override synchronous initial attributes before the WebSocket span is created |
241
- | `requestHook` | `(span, req) => void \| Promise<void>` | `undefined` | Customize WebSocket span before connection request |
242
- | `responseHook` | `(span, session, req) => void \| Promise<void>` | `undefined` | Customize WebSocket span after session returned; `req` is the original `HttpRequest` |
243
-
244
- > **Hook timing and failures**: Span hooks run only when a `tracer` is supplied. `startSpanHook(request)` is synchronous and runs before span creation. Its attributes are applied after built-in attributes. If it throws, Defjs creates the span with built-in attributes and the client request/stream/session continues normally. `requestHook` and `responseHook` run after creation and may return `void` or `Promise<void>`; neither is awaited, and their throws/rejections do not block or fail the operation. While `span.isRecording()` is true, hook failures record `defjs.otel.hook.error` and `exception` events. For non-recording spans (ended or unsampled), failures go through `diag.error` with a fixed message and `hook.name`, `error.type`, `traceId`, and `spanId`; error messages, stacks, and request data are not included. Applications must configure `diag.setLogger(...)` from `@opentelemetry/api` to receive these diagnostics; there is no automatic output by default. This package does not initialize the SDK, and this diagnostic is not a separate SDK `LogRecord`. This diagnostic includes `hook.name`, `error.type`, `traceId`, and `spanId`, but excludes the original error message or stack and request data.
245
- >
246
- > The HTTP span ends when the synchronous part of `responseHook` returns and is not extended for an asynchronous hook. The synchronous part can still modify a recording span; events cannot be appended after it ends. A later hook rejection uses the diagnostic logger described above.
247
- >
248
- > Each `responseHook` receives the original transport `HttpRequest` as its third argument. Read its explicit `operation`; do not reconstruct identity from its endpoint or a resolved URL.
249
- >
250
- > **Attribute hygiene**: This package has no built-in redactor or sensitive-key policy. Prefer explicit allowlists, application-owned redaction, and stable low-cardinality attributes in hooks. Do not attach resolved URLs, raw query strings, request or response bodies, full headers, baggage values, or message payloads unless your application has already reviewed privacy, cardinality, retention, and redaction requirements.
251
-
252
- ## HTTP Semantic Conventions and Metrics
253
-
254
- HTTP tracing uses stable OpenTelemetry HTTP client attribute and metric names. This is not a claim of complete semantic-convention compliance. With an explicit operation, the interceptor creates a `SpanKind.CLIENT` span named `${method} ${operation}` and records `defjs.operation`. Without one, it preserves the previous method-only span name and omits that attribute. It also records:
255
-
256
- - `http.request.method`
257
- - `url.full`
258
- - `server.address`
259
- - Optional `server.port` — only when the URL has an explicit non-default port
260
- - `http.response.status_code` only when a response status was received
261
-
262
- HTTP span status and `error.type` follow these rules:
263
-
264
- - status `100` through `399` leaves span status unset and does not set `error.type`;
265
- - status `400` and above marks the client span `ERROR` and sets `error.type` to the status code string;
266
- - a rejection produces no response, so `http.response.status_code` stays unset. This covers a network failure, caller cancellation, a timeout, and a declared-body read or parse failure;
267
- - an error thrown through the interceptor marks the span `ERROR`, records the exception, and uses its `Error.name` or another low-cardinality type fallback as `error.type`. That is `TypeError` for a typical network failure, `Error` for caller cancellation (whatever the abort reason) and for a timeout, and `BodyFailure` for a body failure. It is never a `NET_*` or `RES_*` fault code, because Core classifies faults after the interceptor chain. Cancellation is therefore also an `ERROR` span (it only skips the log).
268
-
269
- In the current implementation, `url.full` is resolved from `req.endpoint` and the optional `req.baseEndpoint`; it does not append `req.queryString`. This is an implementation boundary, not sanitization, an identity source, or a guarantee that URLs are safe. Applications that need a complete or redacted URL can override the initial value explicitly:
270
-
271
- ```ts
272
- import { createResolvedRequestUrl } from "@defjs/core";
273
-
274
63
  withOpenTelemetryServer({
275
- tracer,
276
- http: {
277
- startSpanHook(req) {
278
- if (!req.baseEndpoint) return {};
279
- const url = createResolvedRequestUrl(req.baseEndpoint, req.endpoint);
280
- if (req.queryString) url.search = req.queryString;
281
- url.searchParams.delete("access_token");
282
- return { "url.full": url.href };
283
- },
284
- },
285
- });
286
- ```
287
-
288
- Resolved paths can contain sensitive or high-cardinality identifiers. Keep operation names static and apply application or SDK/exporter redaction before exporting URL attributes.
289
-
290
- When `meter` is provided, the following stable metrics are collected:
291
-
292
- | Metric | Unit | Attributes |
293
- | ------------------------------ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
294
- | `http.client.request.duration` | `s` | `http.request.method`, optional `defjs.operation`, `server.address`, optional `server.port` (explicit non-default URL port only), optional `http.response.status_code`, optional `error.type` |
295
-
296
- The metric applies the same response-status and `error.type` classification as the HTTP span.
297
-
298
- ### Transport span boundary
299
-
300
- The HTTP span ends when the interceptor receives the Defjs `HttpResponse`. For a declared `output` or `error` body, the transport reads the selected JSON, text, blob, or bytes representation before returning that response. Only JSON representation checks the JSON media type and JSON-parses the body. A read failure in any representation, or a JSON media-type / parse failure (`NET_BODY_INCOMPLETE`, `RES_MEDIA_TYPE_INVALID`, or `RES_DECODE_FAILED`), rejects through the interceptor, so the span ends `ERROR` with `error.type` `BodyFailure` and no `http.response.status_code`, whatever the HTTP status. HTTP Struct validation (`RES_STRUCT_MISMATCH`), an absent declared body, and the conversion of a non-2xx response into `HTTP_STATUS` happen afterward and cannot update that transport span.
301
-
302
- When telemetry must represent the final command outcome, create an application span around `client.execute(...)` and classify the returned tuple:
303
-
304
- ```ts
305
- import { SpanStatusCode } from "@opentelemetry/api";
306
-
307
- const outcome = await tracer.startActiveSpan("defjs.command", async (span) => {
308
- try {
309
- const outcome = await client.execute(command);
310
- const [error] = outcome;
311
- if (error) {
312
- span.setAttribute("error.type", error.code);
313
- span.setStatus({ code: SpanStatusCode.ERROR });
314
- }
315
- return outcome;
316
- } catch (error) {
317
- span.setAttribute(
318
- "error.type",
319
- error instanceof Error ? error.name : typeof error,
320
- );
321
- span.setStatus({ code: SpanStatusCode.ERROR });
322
- throw error;
323
- } finally {
324
- span.end();
325
- }
64
+ tracer, // optional: local spans and span hooks
65
+ meter, // optional: metrics
66
+ logger, // optional: failure diagnostics only
67
+ propagator, // optional: defaults to W3C TraceContext + Baggage
68
+ requireParentSpan: false,
69
+ http: { enabled: true, startSpanHook, requestHook, responseHook },
70
+ sse: { enabled: true, startSpanHook, requestHook, responseHook },
71
+ webSocket: { enabled: true, queryPropagation: false },
326
72
  });
327
73
  ```
328
74
 
329
- This outer span is application-owned command telemetry; the plugin continues to emit only its lower-level transport span.
330
-
331
- This package does not add request or response bodies, full headers, baggage values, payload sizes, or message payloads as default custom telemetry fields. It also does not create separate span attributes or metrics for raw query strings.
332
-
333
- Do not add raw query strings, request or response bodies, full headers, baggage values, or message payloads to spans or metrics unless the application has reviewed privacy, cardinality, retention, and redaction requirements. Prefer explicit allowlists, redaction, and stable low-cardinality attributes when extending telemetry with hooks.
334
-
335
- ## SSE Connection-Level Tracing and Custom Metrics
336
-
337
- SSE is a long-lived HTTP response. Normal HTTP request duration ends at stream establishment, which does not reflect whether the stream is still running, interrupted, or errored. Therefore, this package treats SSE as **connection-level** telemetry.
338
-
339
- ### Span Lifecycle
340
-
341
- After startup succeeds, the SSE interceptor attaches to `stream.closed` and keeps the span open until the stream reaches its normal, fatal, or aborted terminal state. It records:
342
-
343
- - `sse.connected` — Stream successfully established
344
- - `sse.closed` — Stream normal end (server EOF)
345
- - `sse.aborted` — Active close via `stream.close()` or a caller abort of the open stream; the span still ends without error
346
- - `sse.error` — Connection error or reconnect exhaustion
347
-
348
- ### Custom Metrics
349
-
350
- When `meter` is provided, the following defjs custom metrics are collected (non-official OpenTelemetry stable semantic conventions):
351
-
352
- | Metric | Unit | Meaning |
353
- | -------------------------------------- | ---------- | ------------------------------------------------------- |
354
- | `defjs.client.sse.connect.duration` | `s` | Time to establish stream connection |
355
- | `defjs.client.sse.connection.duration` | `s` | Total duration from stream establishment to close/error |
356
- | `defjs.client.sse.active_streams` | `{stream}` | Current active SSE stream count |
357
-
358
- By default, **per-event spans are not created**, and **event payloads, event IDs, `Last-Event-ID`, delivery latency, lost events, or reconnect queues are not collected**. These are application-level semantics that may produce high-cardinality or sensitive telemetry. Implement them at the application layer if needed.
359
-
360
- ## WebSocket Connection-Level Tracing and Custom Metrics
361
-
362
- WebSocket starts with an HTTP Upgrade handshake, but production environments care more about the post-handshake connection lifecycle: active connections, connection duration, close/error behavior, and connection failure rate. Since OpenTelemetry WebSocket semantic conventions are not yet stable, this package uses connection-level custom metrics.
363
-
364
- ### Span Lifecycle
365
-
366
- The WebSocket span stays open until `session.closed` resolves, recording the following lifecycle events:
75
+ | Option | Type | Default | Description |
76
+ | ------------------- | --------------------------------------- | -------------------------- | --------------------------------------------------------------------------- |
77
+ | `tracer` | `Tracer` | `undefined` | External tracer; omitting disables local spans and hooks |
78
+ | `meter` | `Meter` | `undefined` | External meter; omitting disables metrics |
79
+ | `logger` | `Logger` from `@opentelemetry/api-logs` | `undefined` | External native Logger; omitting disables diagnostic LogRecords |
80
+ | `propagator` | `TextMapPropagator` | W3C TraceContext + Baggage | Custom context propagator |
81
+ | `requireParentSpan` | `boolean` | `false` | With a tracer, bypass all instrumentation when no active parent span exists |
82
+ | `http` | `OpenTelemetryServerHttpOptions` | `{}` | HTTP transport options |
83
+ | `sse` | `OpenTelemetryServerSSEOptions` | `{}` | SSE transport options |
84
+ | `webSocket` | `OpenTelemetryServerWebSocketOptions` | `{}` | WebSocket transport options |
367
85
 
368
- - `websocket.connected` — Session successfully established
369
- - `websocket.closed` — Connection normal close. A close frame from the server counts as normal whatever its code, including 1011. An abrupt drop (1006) is normal only where the runtime reports no socket `error` before `close` (Bun); where it does (Node.js), the session ends in error instead
370
- - `websocket.error` — Connection error, or a caller abort of an open session. The span ends `ERROR`. A connection error emits one Log; a caller abort emits none. After an abort, `error.type` follows the abort reason: its `Error.name` (`AbortError` for a default `abort()`, `Error` for a plain `Error` or a `null` reason), or its `typeof` (`string`) for a non-Error reason. An abort while the WebSocket is still connecting ends the span `ERROR` with `error.type` `DefjsFault`
86
+ Span hooks run only with a `tracer`. `startSpanHook(request)` is synchronous and runs before span creation; a throw leaves the built-in attributes and the operation continues. `requestHook` and `responseHook` are not awaited, and their failures never block or fail the operation. Configure `diag.setLogger(...)` to see hook diagnostics.
371
87
 
372
- ### Custom Metrics
88
+ ## Privacy defaults
373
89
 
374
- When `meter` is provided, the following defjs custom metrics are collected:
90
+ - `webSocket.queryPropagation` defaults to `false`: trace context and baggage do not ride on the WebSocket URL. Enable it only after reviewing how your proxies, browsers, and access logs record query strings.
91
+ - No bodies, full headers, baggage values, or message payloads are added as telemetry fields, and no built-in redactor exists. Prefer allowlists and low-cardinality attributes in hooks.
92
+ - Diagnostics carry a fixed message, phase, operation, server address/port, and exception type — not raw URLs, headers, bodies, or stacks.
93
+ - SSE and WebSocket are traced per connection, not per event or message.
375
94
 
376
- | Metric | Unit | Meaning |
377
- | -------------------------------------------- | -------------- | -------------------------------------------------------- |
378
- | `defjs.client.websocket.connect.duration` | `s` | Time to establish WebSocket session |
379
- | `defjs.client.websocket.connection.duration` | `s` | Total duration from session establishment to close/error |
380
- | `defjs.client.websocket.active_connections` | `{connection}` | Current active WebSocket connection count |
95
+ ## Documentation
381
96
 
382
- By default, **per-message spans are not created**, and **message payloads, message sizes, backpressure, buffered amount, subprotocols, or reconnect queues are not collected**. Keep any application-layer message telemetry opt-in, sampled, redacted, and limited to stable low-cardinality attributes whenever possible.
97
+ - [OpenTelemetry guide](https://defjs.lyz.cloud/docs/integrations/opentelemetry-server) — full option tables, log events, span lifecycles, metrics, and third-party logging
98
+ - [OpenTelemetry API](https://defjs.lyz.cloud/api/opentelemetry-server)
99
+ - [Client](https://defjs.lyz.cloud/docs/client) · [SSE](https://defjs.lyz.cloud/docs/sse) · [WebSocket](https://defjs.lyz.cloud/docs/web-socket)
383
100
 
384
- ## What's Next
101
+ ## License
385
102
 
386
- - [OpenTelemetry guide](https://defjs.lyz.cloud/docs/integrations/opentelemetry-server) — complete plugin boundaries and examples
387
- - [Client](https://defjs.lyz.cloud/docs/client) — `createClient` and full transport configuration
388
- - [SSE](https://defjs.lyz.cloud/docs/sse) — `defineEventStream` and streaming event consumption
389
- - [WebSocket](https://defjs.lyz.cloud/docs/web-socket) — `defineWebSocket` and real-time communication
103
+ MIT.
package/index.d.ts CHANGED
@@ -39,7 +39,7 @@ interface OpenTelemetryServerTransportOptions<TResponse> {
39
39
  *
40
40
  * - The HTTP span ends as soon as `responseHook` returns; the hook is not awaited, so only its synchronous part can still change the span. A status of 400 or above ends it `ERROR` with `error.type` set to the status string; 100–399 leaves its status unset. An asynchronous hook rejection after end is reported through `diag.error`; configure `diag.setLogger(...)` from `@opentelemetry/api` to receive it. It cannot append events to an ended span.
41
41
  *
42
- * - A rejection through the interceptor has no response status and does not call `responseHook`. That includes a network failure, cancellation, timeout, any declared-body read failure (`BodyFailure`, including text, blob, and bytes, not only JSON), and a progress observer failure (`EXT_OBSERVER_FAILED`). The span ends `ERROR` with `error.type` set to the thrown class name, such as `TypeError`, `Error` or `BodyFailure`. Caller cancellation is `Error` whatever the abort reason.
42
+ * - A rejection through the interceptor has no response status and does not call `responseHook`. That includes a network failure, caller cancellation, an abort whose reason is `TimeoutError`, any declared-body read failure (`BodyFailure`, including text, blob, and bytes, not only JSON), and a progress observer failure (`EXT_OBSERVER_FAILED`). The span ends `ERROR` with `error.type` set to the thrown class name, such as `TypeError`, `Error` or `BodyFailure`. Caller cancellation is `Error` whatever the abort reason. There is no `timeout` option; a deadline is `abort`.
43
43
  *
44
44
  * Pass `{ enabled: false }` to skip HTTP instrumentation. Span hooks run only when a `tracer` is supplied.
45
45
  *
@@ -54,7 +54,7 @@ type OpenTelemetryServerHttpOptions = OpenTelemetryServerTransportOptions<HttpRe
54
54
  *
55
55
  * - `responseHook(span, stream, req)` runs once the stream has opened, before the span records `sse.connected`. `stream` is the `EventStreamHandle` the transport returned; `req` is the original `HttpRequest`.
56
56
  *
57
- * - SSE is traced at connection level. After startup the span stays open until `stream.closed` settles, and records `sse.connected`, then one of `sse.closed` (server EOF), `sse.aborted` (`stream.close()` or a non-timeout caller abort of the open stream; the span still ends without error), or `sse.error` (including a timeout-shaped abort, which ends the span `ERROR`). No per-event spans are created.
57
+ * - SSE is traced at connection level. After startup the span stays open until `stream.closed` settles, and records `sse.connected`, then one of `sse.closed` (server EOF), `sse.aborted` (`stream.close()` or a caller abort of the open stream whose reason is not `TimeoutError`; the span still ends without error), or `sse.error` (including an abort whose reason is `TimeoutError`, which ends the span `ERROR`). No per-event spans are created.
58
58
  *
59
59
  * - A non-2xx handshake is observed as a connection failure: the span ends `ERROR` and, with a `logger`, one `WARN` is emitted whatever the response body.
60
60
  *
package/index.js CHANGED
@@ -270,11 +270,11 @@ const CLIENT_EXCEPTIONS = {
270
270
  }
271
271
  };
272
272
  /**
273
- * Caller cancellation produces no log. A timeout still logs.
273
+ * Caller cancellation produces no log. An abort whose reason is `TimeoutError` or `ERR_TIMEOUT` still logs.
274
274
  *
275
- * The request signal alone is not cancellation: WebSocket `finish()` aborts that same signal
275
+ * `request.abort` alone is not cancellation: WebSocket `finish()` aborts that same signal
276
276
  * when a connect fails. Cancellation is an abort sentinel from any `@defjs/core` copy, or an
277
- * `AbortError`, on the rejection or on the signal reason. `TimeoutError` and `ERR_TIMEOUT` log.
277
+ * `AbortError`, on the rejection or on `request.abort.reason`. There is no `timeout` option.
278
278
  */
279
279
  function isCallerCancellation(request, error) {
280
280
  const reason = request.abort?.reason;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defjs/opentelemetry-server",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Append OpenTelemetry interceptors to an application-owned Defjs client.",
5
5
  "bugs": {
6
6
  "url": "https://github.com/defjs/defjs/issues"
@@ -17,7 +17,7 @@
17
17
  "@opentelemetry/api-logs": "^0.223.0"
18
18
  },
19
19
  "peerDependencies": {
20
- "@defjs/core": "^0.4.0",
20
+ "@defjs/core": "^0.5.0",
21
21
  "@opentelemetry/api": "^1.0.0",
22
22
  "@opentelemetry/core": "^2.0.0 || ^3.0.0"
23
23
  },