@defjs/opentelemetry-server 0.0.1-beta.0 → 0.0.1

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.
Files changed (4) hide show
  1. package/README.md +165 -66
  2. package/index.d.ts +103 -18
  3. package/index.js +238 -109
  4. package/package.json +7 -3
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # @defjs/opentelemetry-server
2
2
 
3
- Server-side outbound OpenTelemetry integration for `@defjs/core` HTTP, SSE, and WebSocket clients.
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.
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.
4
6
 
5
7
  **Core Positioning**:
6
8
 
7
9
  - **Outbound-only client instrumentation** — This package wraps defjs clients that make outbound requests. It is not inbound server instrumentation.
8
10
  - **Server environment** — Designed for server-side applications with an OpenTelemetry SDK, not browser-only telemetry setup.
9
- - **Does not initialize the SDK** — This package does not initialize an OpenTelemetry SDK. Register a `TracerProvider` (`setGlobalTracerProvider`, or your SDK's `start()`) then pass that tracer (and optional meter) into `withOpenTelemetryServer(...)`. `trace.getTracer(...)` with no provider is a silent no-op.
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.
10
12
  - **Per-transport separation** — HTTP, SSE, and WebSocket each have independent interceptors, span lifecycles, and metric dimensions.
11
13
 
12
14
  ## Installation
@@ -19,39 +21,127 @@ bun add @defjs/core @defjs/opentelemetry-server @opentelemetry/api @opentelemetr
19
21
 
20
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.
21
23
 
22
- Initialize your application's OpenTelemetry SDK separately so a `TracerProvider` is registered. The package provides interceptors and consumes the tracer you pass; it does not emit spans on its own.
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.
23
25
 
24
26
  ## Basic Usage
25
27
 
26
- 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:
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:
27
29
 
28
30
  ```typescript
29
- import { createClient, withEndpoint } from '@defjs/core'
30
- import { withOpenTelemetryServer } from '@defjs/opentelemetry-server'
31
- import { trace } from '@opentelemetry/api'
31
+ import { createClient, withEndpoint } from "@defjs/core";
32
+ import { withOpenTelemetryServer } from "@defjs/opentelemetry-server";
33
+ import { trace } from "@opentelemetry/api";
32
34
 
33
35
  // After your OpenTelemetry SDK has called setGlobalTracerProvider:
34
- const tracer = trace.getTracer('my-service')
36
+ const tracer = trace.getTracer("my-service");
35
37
 
36
38
  const client = createClient(
37
- withEndpoint('https://api.example.com'),
39
+ withEndpoint("https://api.example.com"),
38
40
  withOpenTelemetryServer({
39
41
  tracer,
40
42
  }),
41
- )
43
+ );
44
+ ```
45
+
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:
49
+
50
+ ```typescript
51
+ import { createClient, defineRequest, withEndpoint } from "@defjs/core";
52
+ import { withOpenTelemetryServer } from "@defjs/opentelemetry-server";
53
+ import { metrics } from "@opentelemetry/api";
54
+
55
+ const meter = metrics.getMeter("my-service");
56
+ const readOrders = defineRequest({
57
+ method: "GET",
58
+ operation: "orders.read",
59
+ path: "/orders",
60
+ });
61
+
62
+ const client = createClient(
63
+ withEndpoint("https://api.example.com"),
64
+ withOpenTelemetryServer({ meter }),
65
+ );
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().
42
106
  ```
43
107
 
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.
123
+
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
+
44
134
  ## Operation Identity
45
135
 
46
136
  Set the optional `operation` field statically on `defineRequest(...)`, `defineEventStream(...)`, or `defineWebSocket(...)`:
47
137
 
48
138
  ```typescript
49
139
  const readOrder = defineRequest({
50
- method: 'GET',
51
- operation: 'orders.read',
52
- path: '/orders/:id',
140
+ method: "GET",
141
+ operation: "orders.read",
142
+ path: "/orders/:id",
53
143
  // input and output omitted
54
- })
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`.
@@ -62,19 +152,20 @@ Without `operation`, the previous fallback remains unchanged: HTTP uses the requ
62
152
 
63
153
  ```typescript
64
154
  const client = createClient(
65
- withEndpoint('https://api.example.com'),
155
+ withEndpoint("https://api.example.com"),
66
156
  withOpenTelemetryServer({
67
- tracer, // Required
157
+ tracer, // Optional, local spans and span hooks only run when provided
68
158
  meter, // Optional, metrics only collected when provided
159
+ logger, // Optional native OTel Logger, failure diagnostics only
69
160
  propagator, // Optional, default W3C TraceContext + Baggage
70
161
  requireParentSpan: false,
71
162
  http: {
72
163
  enabled: true,
73
164
  startSpanHook(req) {
74
- return { 'app.operation': req.operation ?? 'unclassified' }
165
+ return { "app.operation": req.operation ?? "unclassified" };
75
166
  },
76
167
  responseHook(span, res, req) {
77
- span.setAttribute('defjs.response.status_text', res.statusText)
168
+ span.setAttribute("defjs.response.status_text", res.statusText);
78
169
  },
79
170
  },
80
171
  sse: {
@@ -85,7 +176,7 @@ const client = createClient(
85
176
  queryPropagation: false, // Safe default; set true only after reviewing URL propagation
86
177
  },
87
178
  }),
88
- )
179
+ );
89
180
  ```
90
181
 
91
182
  ## WebSocket propagation safety
@@ -100,30 +191,33 @@ withOpenTelemetryServer({
100
191
  webSocket: {
101
192
  queryPropagation: true,
102
193
  },
103
- })
194
+ });
104
195
  ```
105
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
+
106
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.
107
200
 
108
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.
109
202
 
110
203
  ### Configuration Options
111
204
 
112
- | Option | Type | Default | Description |
113
- | ------------------- | ------------------------------------- | -------------------------- | ------------------------------------------------------------ |
114
- | `tracer` | `Tracer` | **Required** | External OpenTelemetry tracer |
115
- | `meter` | `Meter` | `undefined` | External OpenTelemetry meter, omitting disables metrics |
116
- | `propagator` | `TextMapPropagator` | W3C TraceContext + Baggage | Custom context propagator |
117
- | `requireParentSpan` | `boolean` | `false` | Only create outbound spans when an active parent span exists |
118
- | `http` | `OpenTelemetryServerHttpOptions` | `{}` | HTTP transport trace/metric options |
119
- | `sse` | `OpenTelemetryServerSSEOptions` | `{}` | SSE transport trace/metric options |
120
- | `webSocket` | `OpenTelemetryServerWebSocketOptions` | `{}` | WebSocket transport trace/metric options |
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 |
121
215
 
122
216
  ### HTTP Options
123
217
 
124
218
  | Option | Type | Default | Description |
125
219
  | --------------- | ------------------------------------------- | ----------- | ------------------------------------------------------------------------------ |
126
- | `enabled` | `boolean` | `true` | Enable HTTP tracing |
220
+ | `enabled` | `boolean` | `true` | Enable HTTP instrumentation |
127
221
  | `startSpanHook` | `(req) => Attributes` | `undefined` | Add or override synchronous initial attributes before the HTTP span is created |
128
222
  | `requestHook` | `(span, req) => void \| Promise<void>` | `undefined` | Customize HTTP span before request, `req` is `HttpRequest` |
129
223
  | `responseHook` | `(span, res, req) => void \| Promise<void>` | `undefined` | Customize HTTP span after response; `req` is the original `HttpRequest` |
@@ -132,7 +226,7 @@ With the safe default, trace context does not ride on the WebSocket URL. Use ano
132
226
 
133
227
  | Option | Type | Default | Description |
134
228
  | --------------- | ---------------------------------------------- | ----------- | ------------------------------------------------------------------------------------ |
135
- | `enabled` | `boolean` | `true` | Enable SSE tracing |
229
+ | `enabled` | `boolean` | `true` | Enable SSE instrumentation |
136
230
  | `startSpanHook` | `(req) => Attributes` | `undefined` | Add or override synchronous initial attributes before the SSE span is created |
137
231
  | `requestHook` | `(span, req) => void \| Promise<void>` | `undefined` | Customize SSE span before stream request |
138
232
  | `responseHook` | `(span, stream, req) => void \| Promise<void>` | `undefined` | Customize SSE span after stream handle returned; `req` is the original `HttpRequest` |
@@ -141,13 +235,15 @@ With the safe default, trace context does not ride on the WebSocket URL. Use ano
141
235
 
142
236
  | Option | Type | Default | Description |
143
237
  | ------------------ | ----------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------- |
144
- | `enabled` | `boolean` | `true` | Enable WebSocket tracing |
238
+ | `enabled` | `boolean` | `true` | Enable WebSocket instrumentation |
145
239
  | `queryPropagation` | `boolean` | `false` | Inject trace context and baggage into the WebSocket URL query string. Enable only after reviewing URL logging and disclosure risks. |
146
240
  | `startSpanHook` | `(req) => Attributes` | `undefined` | Add or override synchronous initial attributes before the WebSocket span is created |
147
241
  | `requestHook` | `(span, req) => void \| Promise<void>` | `undefined` | Customize WebSocket span before connection request |
148
242
  | `responseHook` | `(span, session, req) => void \| Promise<void>` | `undefined` | Customize WebSocket span after session returned; `req` is the original `HttpRequest` |
149
243
 
150
- > **Hook timing and failures**: `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, records `defjs.otel.hook.error`, and the client request/stream/session continues normally. `requestHook` and `responseHook` run after creation and may return `void` or `Promise<void>`; their throws/rejections are recorded without blocking the operation.
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.
151
247
  >
152
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.
153
249
  >
@@ -160,71 +256,74 @@ HTTP tracing uses stable OpenTelemetry HTTP client attribute and metric names. T
160
256
  - `http.request.method`
161
257
  - `url.full`
162
258
  - `server.address`
163
- - `server.port`
259
+ - Optional `server.port` — only when the URL has an explicit non-default port
164
260
  - `http.response.status_code` only when a response status was received
165
261
 
166
262
  HTTP span status and `error.type` follow these rules:
167
263
 
168
264
  - status `100` through `399` leaves span status unset and does not set `error.type`;
169
265
  - status `400` and above marks the client span `ERROR` and sets `error.type` to the status code string;
170
- - a Defjs status-0 transport result does not set `http.response.status_code`; caller cancellation leaves status unset, timeout uses `ERROR` / `TIMEOUT`, and other transport failures use `ERROR` / `NETWORK_ERROR`;
171
- - 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`.
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).
172
268
 
173
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:
174
270
 
175
271
  ```ts
176
- import { createResolvedRequestUrl } from '@defjs/core'
272
+ import { createResolvedRequestUrl } from "@defjs/core";
177
273
 
178
274
  withOpenTelemetryServer({
179
275
  tracer,
180
276
  http: {
181
277
  startSpanHook(req) {
182
- if (!req.baseEndpoint) return {}
183
- const url = createResolvedRequestUrl(req.baseEndpoint, req.endpoint)
184
- if (req.queryString) url.search = req.queryString
185
- url.searchParams.delete('access_token')
186
- return { 'url.full': url.href }
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 };
187
283
  },
188
284
  },
189
- })
285
+ });
190
286
  ```
191
287
 
192
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.
193
289
 
194
290
  When `meter` is provided, the following stable metrics are collected:
195
291
 
196
- | Metric | Unit | Attributes |
197
- | ------------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
198
- | `http.client.request.duration` | `s` | `http.request.method`, optional `defjs.operation`, `server.address`, `server.port`, optional `http.response.status_code`, optional `error.type` |
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` |
199
295
 
200
296
  The metric applies the same response-status and `error.type` classification as the HTTP span.
201
297
 
202
298
  ### Transport span boundary
203
299
 
204
- The HTTP span ends when the interceptor receives the Defjs `HttpResponse`. Exact output-status dispatch and Struct decoding happen afterward, so a later `RESPONSE_VALIDATION_FAILED` or `UNDECLARED_STATUS` cannot update that transport span.
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.
205
301
 
206
302
  When telemetry must represent the final command outcome, create an application span around `client.execute(...)` and classify the returned tuple:
207
303
 
208
304
  ```ts
209
- import { SpanStatusCode } from '@opentelemetry/api'
305
+ import { SpanStatusCode } from "@opentelemetry/api";
210
306
 
211
- const outcome = await tracer.startActiveSpan('defjs.command', async (span) => {
307
+ const outcome = await tracer.startActiveSpan("defjs.command", async (span) => {
212
308
  try {
213
- const outcome = await client.execute(command)
214
- const [error] = outcome
309
+ const outcome = await client.execute(command);
310
+ const [error] = outcome;
215
311
  if (error) {
216
- span.setAttribute('error.type', error.code)
217
- span.setStatus({ code: SpanStatusCode.ERROR })
312
+ span.setAttribute("error.type", error.code);
313
+ span.setStatus({ code: SpanStatusCode.ERROR });
218
314
  }
219
- return outcome
315
+ return outcome;
220
316
  } catch (error) {
221
- span.setAttribute('error.type', error instanceof Error ? error.name : typeof error)
222
- span.setStatus({ code: SpanStatusCode.ERROR })
223
- throw 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;
224
323
  } finally {
225
- span.end()
324
+ span.end();
226
325
  }
227
- })
326
+ });
228
327
  ```
229
328
 
230
329
  This outer span is application-owned command telemetry; the plugin continues to emit only its lower-level transport span.
@@ -243,7 +342,7 @@ After startup succeeds, the SSE interceptor attaches to `stream.closed` and keep
243
342
 
244
343
  - `sse.connected` — Stream successfully established
245
344
  - `sse.closed` — Stream normal end (server EOF)
246
- - `sse.aborted` — Active close via `stream.close()`
345
+ - `sse.aborted` — Active close via `stream.close()` or a caller abort of the open stream; the span still ends without error
247
346
  - `sse.error` — Connection error or reconnect exhaustion
248
347
 
249
348
  ### Custom Metrics
@@ -267,8 +366,8 @@ WebSocket starts with an HTTP Upgrade handshake, but production environments car
267
366
  The WebSocket span stays open until `session.closed` resolves, recording the following lifecycle events:
268
367
 
269
368
  - `websocket.connected` — Session successfully established
270
- - `websocket.closed` — Connection normal close
271
- - `websocket.error` — Connection error
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`
272
371
 
273
372
  ### Custom Metrics
274
373
 
@@ -284,7 +383,7 @@ By default, **per-message spans are not created**, and **message payloads, messa
284
383
 
285
384
  ## What's Next
286
385
 
287
- - [OpenTelemetry guide](https://defjs.org/plugins/opentelemetry-server) — complete plugin boundaries and examples
288
- - [Client](https://defjs.org/core/client) — `createClient` and full transport configuration
289
- - [SSE](https://defjs.org/core/sse) — `defineEventStream` and streaming event consumption
290
- - [WebSocket](https://defjs.org/core/web-socket) — `defineWebSocket` and real-time communication
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
package/index.d.ts CHANGED
@@ -1,7 +1,25 @@
1
1
  import { ClientOption, EventStreamHandle, HttpRequest, HttpResponse, WebSocketSessionLike } from "@defjs/core";
2
2
  import { Attributes, Meter, Span, TextMapPropagator, Tracer } from "@opentelemetry/api";
3
- //#region src/option.d.ts
4
- /** Shared per-transport OpenTelemetry client options. */
3
+ import { Logger } from "@opentelemetry/api-logs";
4
+ /**
5
+ * Shared per-transport OpenTelemetry client options: whether the transport is enabled and its span hooks, generic over the response type.
6
+ *
7
+ * `TResponse` is what `responseHook` receives: `HttpResponse<unknown>` for HTTP, `EventStreamHandle<unknown>` for SSE, `WebSocketSessionLike` for WebSocket. See {@link OpenTelemetryServerHttpOptions}, {@link OpenTelemetryServerSSEOptions} and {@link OpenTelemetryServerWebSocketOptions}.
8
+ *
9
+ * `enabled` defaults to `true`; only `enabled: false` turns the transport off. The old boolean switch (`http: false`) and top-level hooks are rejected by the TypeScript types only. There is no runtime check, so at runtime `http: false` leaves the transport enabled and a top-level hook is silently ignored.
10
+ *
11
+ * Span hooks run only when a `tracer` is supplied. `startSpanHook` runs synchronously before span creation for its own transport. Its attributes are applied after built-in attributes, so the application may override `url.full` or another initial value. If it throws, Defjs creates the span with built-in attributes and continues the request. `requestHook` runs after span creation and before the request is sent; `responseHook` runs after the transport result is returned. Both may return a promise, but neither is awaited: a throw or rejection never blocks or fails the operation. `responseHook` receives the original request as its third argument.
12
+ *
13
+ * While `span.isRecording()` is true, hook failures record `defjs.otel.hook.error` and `exception` events. For non-recording spans (ended or unsampled), failures go to the OpenTelemetry diagnostic logger through `diag.error`. Applications must configure `diag.setLogger(...)` from `@opentelemetry/api` to receive them; there is no automatic output by default. The package does not initialize the SDK, and this diagnostic is not a separate SDK `LogRecord`.
14
+ *
15
+ * By default, `url.full` resolves only `request.endpoint` against optional `request.baseEndpoint`; an independent `request.queryString` is not appended. That boundary is not redaction, and the package has no built-in redactor or sensitive-key policy. Construct and, if needed, redact an application-owned URL explicitly.
16
+ *
17
+ * @group opentelemetry-server
18
+ * @see {@link OpenTelemetryServerHttpOptions}
19
+ * @see {@link OpenTelemetryServerSSEOptions}
20
+ * @see {@link OpenTelemetryServerWebSocketOptions}
21
+ * @exampleFile examples/api/opentelemetry-server-transport-options.ts#example
22
+ */
5
23
  interface OpenTelemetryServerTransportOptions<TResponse> {
6
24
  /** Whether this transport's instrumentation is enabled. Defaults to `true`. */
7
25
  enabled?: boolean;
@@ -12,8 +30,54 @@ interface OpenTelemetryServerTransportOptions<TResponse> {
12
30
  /** Customize the client span after the transport result is returned. */
13
31
  responseHook?: (span: Span, res: TResponse, req: HttpRequest) => Promise<void> | void;
14
32
  }
33
+ /**
34
+ * Options for the HTTP transport — the shared options with an HttpResponse in responseHook.
35
+ *
36
+ * Set on `http` in `OpenTelemetryServerOptions`. It has no fields of its own: `enabled`, `startSpanHook`, `requestHook` and `responseHook` come from `OpenTelemetryServerTransportOptions`.
37
+ *
38
+ * - `responseHook(span, res, req)` receives the `HttpResponse` that came back through the interceptor, before the client decodes a declared body, together with the original `HttpRequest`. Read `req.operation` for the request identity; do not rebuild it from the endpoint or a resolved URL.
39
+ *
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
+ *
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.
43
+ *
44
+ * Pass `{ enabled: false }` to skip HTTP instrumentation. Span hooks run only when a `tracer` is supplied.
45
+ *
46
+ * @group opentelemetry-server
47
+ * @see {@link OpenTelemetryServerTransportOptions}
48
+ */
15
49
  type OpenTelemetryServerHttpOptions = OpenTelemetryServerTransportOptions<HttpResponse<unknown>>;
50
+ /**
51
+ * Options for the SSE transport — the shared options with the stream handle in responseHook.
52
+ *
53
+ * Set on `sse` in `OpenTelemetryServerOptions`. It has no fields of its own: `enabled`, `startSpanHook`, `requestHook` and `responseHook` come from `OpenTelemetryServerTransportOptions`.
54
+ *
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
+ *
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.
58
+ *
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
+ *
61
+ * Pass `{ enabled: false }` to skip SSE instrumentation. Span hooks run only when a `tracer` is supplied.
62
+ *
63
+ * @group opentelemetry-server
64
+ * @see {@link OpenTelemetryServerTransportOptions}
65
+ */
16
66
  type OpenTelemetryServerSSEOptions = OpenTelemetryServerTransportOptions<EventStreamHandle<unknown>>;
67
+ /**
68
+ * Options for the WebSocket transport — the shared options plus queryPropagation.
69
+ *
70
+ * Set on `webSocket` in `OpenTelemetryServerOptions`. `enabled`, `startSpanHook`, `requestHook` and `responseHook` come from `OpenTelemetryServerTransportOptions`; `responseHook(span, session, req)` receives the opened `WebSocketSessionLike` and the original `HttpRequest`.
71
+ *
72
+ * `queryPropagation` defaults to `false`: the WebSocket query string is left unchanged and no trace context or baggage is written into the URL. Browser WebSocket clients usually cannot set headers, so enabling it writes the propagated context to the end of the query string, replacing any parameter of the same name. Query strings can be recorded by proxies, access logs and APM tools, and baggage values can be sensitive. Turn it on only after you accept that exposure.
73
+ *
74
+ * WebSocket is traced at connection level. The span stays open until `session.closed` settles. It records `websocket.connected`, then `websocket.closed` for a normal close or `websocket.error`. A close frame from the server counts as normal with any code, including 1011. An abrupt drop without a close frame (1006) is normal only where the runtime reports no socket `error` before `close` (Bun); where it does (Node.js), the session ends in error and logs once. A caller abort of an open session is also `websocket.error`: the span ends `ERROR` and no log is emitted. `error.type` follows the abort reason: `AbortError` for a default `abort()`, the `Error.name` of an `Error` reason, `string` for a string reason. An abort while the WebSocket is still connecting ends the span `ERROR` with `error.type` `DefjsFault`. No per-message spans are created.
75
+ *
76
+ * Pass `{ enabled: false }` to skip WebSocket instrumentation. Span hooks run only when a `tracer` is supplied.
77
+ *
78
+ * @group opentelemetry-server
79
+ * @see {@link OpenTelemetryServerTransportOptions}
80
+ */
17
81
  interface OpenTelemetryServerWebSocketOptions extends OpenTelemetryServerTransportOptions<WebSocketSessionLike> {
18
82
  /**
19
83
  * Inject trace context into the WebSocket URL query string.
@@ -22,19 +86,39 @@ interface OpenTelemetryServerWebSocketOptions extends OpenTelemetryServerTranspo
22
86
  queryPropagation?: boolean;
23
87
  }
24
88
  /**
25
- * Options for {@link withOpenTelemetryServer}.
89
+ * Options for {@link withOpenTelemetryServer}: tracer, meter, logger, propagator, and per-transport settings.
26
90
  *
27
- * Pass an application-owned OpenTelemetry `Tracer` (and optional `Meter`).
91
+ * Pass application-owned OpenTelemetry `Tracer`, `Meter`, and/or `Logger` handles.
28
92
  * This package does not initialize the OpenTelemetry SDK.
93
+ *
94
+ * - `tracer` — Optional. Without it there are no local spans and no span hooks.
95
+ *
96
+ * - `meter` — Optional. Without it no package metrics are recorded.
97
+ *
98
+ * - `logger` — Optional. A `Logger` from `@opentelemetry/api-logs`, not a Winston/Pino Logger. Without it no diagnostic LogRecords are emitted. Defjs never obtains a global logger or owns its provider.
99
+ *
100
+ * - `propagator` — Defaults to W3C Trace Context + Baggage.
101
+ *
102
+ * - `requireParentSpan` — Defaults to `false`. With a `tracer`, `true` bypasses all instrumentation, including logs, when no active parent span exists. Without a `tracer` it blocks neither metrics nor logs.
103
+ *
104
+ * - `http`, `sse`, `webSocket` — Per-transport options. Each defaults to `{}`; a transport is disabled only by `{ enabled: false }`.
105
+ *
106
+ * @group opentelemetry-server
107
+ * @see {@link OpenTelemetryServerHttpOptions}
108
+ * @see {@link OpenTelemetryServerSSEOptions}
109
+ * @see {@link OpenTelemetryServerWebSocketOptions}
110
+ * @see {@link OpenTelemetryServerTransportOptions}
29
111
  */
30
112
  interface OpenTelemetryServerOptions {
31
- /** OpenTelemetry tracer used for outbound client spans. */
32
- tracer: Tracer;
113
+ /** Optional tracer; omitting it disables outbound spans and span hooks. */
114
+ tracer?: Tracer;
33
115
  /** Optional meter; when set, per-transport client metrics are recorded. */
34
116
  meter?: Meter;
117
+ /** Optional logger; emits WARN diagnostics for failed outbound requests and connections. */
118
+ logger?: Logger;
35
119
  /** Context propagator. Defaults to W3C Trace Context + Baggage. */
36
120
  propagator?: TextMapPropagator;
37
- /** When `true`, only create an outgoing span if an active parent span exists. */
121
+ /** With a tracer, bypass instrumentation when no active parent span exists. Ignored without a tracer. */
38
122
  requireParentSpan?: boolean;
39
123
  /** HTTP transport instrumentation options. */
40
124
  http?: OpenTelemetryServerHttpOptions;
@@ -46,20 +130,21 @@ interface OpenTelemetryServerOptions {
46
130
  /**
47
131
  * Client option that installs outbound OpenTelemetry interceptors for HTTP, SSE, and WebSocket.
48
132
  *
49
- * Does not initialize the OpenTelemetry SDK — supply a `tracer` (and optional `meter`) from your app.
50
- * Each transport can be toggled or customized via `http`, `sse`, and `webSocket`.
133
+ * Appends one interceptor per enabled transport, in the order HTTP, SSE, WebSocket. Apply it at `createClient` time. It does not initialize the OpenTelemetry SDK or acquire global tracer, meter, or logger handles: pass the ones your application owns. Each transport can be toggled or customized via `http`, `sse`, and `webSocket`.
134
+ *
135
+ * `options` is required. It carries `tracer`, `meter`, `logger`, `propagator`, `requireParentSpan`, and the `http` / `sse` / `webSocket` transport options. Pass `{}` to install the interceptors with propagation only.
136
+ *
137
+ * Each of `tracer`, `meter` and `logger` is optional and works without the others. Without `tracer`, the plugin creates no local spans, never calls `trace.getTracer(...)`, and skips `startSpanHook`, `requestHook` and `responseHook`. Propagation still forwards the extracted parent context.
138
+ *
139
+ * HTTP, SSE, and WebSocket are on by default. Pass `{ enabled: false }` on a transport to skip it.
51
140
  *
52
- * @param options - Tracer, optional meter/propagator, and per-transport settings.
141
+ * @param options - Required settings object. Each of `tracer`, `meter`, `logger`, and `propagator` may be omitted; pass `{}` for propagation only.
53
142
  * @returns A `ClientOption` for `createClient(...)`.
54
143
  *
55
- * @example
56
- * ```ts
57
- * const client = createClient(
58
- * withEndpoint('https://api.example.com'),
59
- * withOpenTelemetryServer({ tracer }),
60
- * )
61
- * ```
144
+ * @group opentelemetry-server
145
+ * @see {@link ClientOption}
146
+ * @see {@link OpenTelemetryServerOptions}
147
+ * @exampleFile examples/api/with-opentelemetry-server.ts#example
62
148
  */
63
149
  export declare function withOpenTelemetryServer(options: OpenTelemetryServerOptions): ClientOption;
64
- //#endregion
65
150
  export type { OpenTelemetryServerHttpOptions, OpenTelemetryServerOptions, OpenTelemetryServerSSEOptions, OpenTelemetryServerTransportOptions, OpenTelemetryServerWebSocketOptions };
package/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator } from "@opentelemetry/core";
2
- import { ERR_ABORTED, createHttpInterceptor, createResolvedRequestUrl, createSSEInterceptor, createTransportError, createWebSocketInterceptor } from "@defjs/core";
3
- import { SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
4
- //#region src/propagation/carrier.ts
2
+ import { ERR_ABORTED, createHttpInterceptor, createResolvedRequestUrl, createSSEInterceptor, createWebSocketInterceptor, isAbortCause, isTimeoutCause } from "@defjs/core";
3
+ import { SpanKind, SpanStatusCode, context, diag, trace } from "@opentelemetry/api";
4
+ import { SeverityNumber } from "@opentelemetry/api-logs";
5
+ // src/propagation/carrier.ts
5
6
  const headersSetter = { set(carrier, key, value) {
6
7
  if (carrier && key && value !== void 0) carrier.set(key, String(value));
7
8
  } };
@@ -16,10 +17,9 @@ const headersGetter = {
16
17
  }
17
18
  };
18
19
  const queryStringSetter = { set(carrier, key, value) {
19
- if (carrier && key && value !== void 0) carrier.params.append(key, String(value));
20
+ if (carrier && key && value !== void 0) carrier.params.set(key, String(value));
20
21
  } };
21
- //#endregion
22
- //#region src/telemetry/trace.ts
22
+ // src/telemetry/trace.ts
23
23
  const spanStatusSet = /* @__PURE__ */ new WeakMap();
24
24
  function markSpanStatusSet(span) {
25
25
  spanStatusSet.set(span, true);
@@ -69,9 +69,9 @@ function createWebSocketSpan(tracer, url, parentCtx, operation, startAttributes)
69
69
  }
70
70
  }, parentCtx);
71
71
  }
72
- function setSpanHttpResponse(span, status, error) {
72
+ function setSpanHttpResponse(span, status) {
73
73
  if (status !== 0) span.setAttribute("http.response.status_code", status);
74
- const errorType = getHttpResponseErrorType(status, error);
74
+ const errorType = getHttpResponseErrorType(status);
75
75
  if (errorType) {
76
76
  span.setAttribute("error.type", errorType);
77
77
  span.setStatus({ code: SpanStatusCode.ERROR });
@@ -100,6 +100,18 @@ function runSpanHook(span, hookName, hook) {
100
100
  }
101
101
  }
102
102
  function recordSpanHookError(span, hookName, error) {
103
+ if (!span.isRecording()) {
104
+ try {
105
+ const { traceId, spanId } = span.spanContext();
106
+ diag.error("@defjs/opentelemetry-server: span hook failed on a non-recording span", {
107
+ "hook.name": hookName,
108
+ "error.type": getErrorType(error),
109
+ traceId,
110
+ spanId
111
+ });
112
+ } catch {}
113
+ return;
114
+ }
103
115
  try {
104
116
  span.addEvent("defjs.otel.hook.error", {
105
117
  "hook.name": hookName,
@@ -120,11 +132,7 @@ function addSpanEvent(span, eventName, attributes) {
120
132
  function toError(error) {
121
133
  return error instanceof Error ? error : new Error(String(error));
122
134
  }
123
- function getHttpResponseErrorType(status, error) {
124
- if (status === 0) {
125
- const errorCode = createTransportError(error).code;
126
- return errorCode === "ABORTED" ? void 0 : errorCode;
127
- }
135
+ function getHttpResponseErrorType(status) {
128
136
  if (status >= 400) return String(status);
129
137
  }
130
138
  function getErrorType(error) {
@@ -132,8 +140,7 @@ function getErrorType(error) {
132
140
  if (error === void 0 || error === null) return "_OTHER";
133
141
  return typeof error;
134
142
  }
135
- //#endregion
136
- //#region src/telemetry/url.ts
143
+ // src/telemetry/url.ts
137
144
  function resolveUrl(endpoint, baseEndpoint) {
138
145
  if (!baseEndpoint) return endpoint;
139
146
  try {
@@ -155,8 +162,7 @@ function resolveHttpUrl(endpoint, baseEndpoint) {
155
162
  return { url: endpoint };
156
163
  }
157
164
  }
158
- //#endregion
159
- //#region src/telemetry/metrics.ts
165
+ // src/telemetry/metrics.ts
160
166
  const DURATION_BUCKETS = [
161
167
  .005,
162
168
  .01,
@@ -224,7 +230,7 @@ function createHttpMetricAttributes(req, response, error) {
224
230
  ...createServerMetricAttributes(req),
225
231
  "http.request.method": req.method
226
232
  };
227
- const errorType = response ? getHttpResponseErrorType(response.status, response.error) : createErrorMetricAttributes(error)["error.type"];
233
+ const errorType = response ? getHttpResponseErrorType(response.status) : createErrorMetricAttributes(error)["error.type"];
228
234
  if (errorType) attributes["error.type"] = errorType;
229
235
  if (response && response.status !== 0) attributes["http.response.status_code"] = response.status;
230
236
  return attributes;
@@ -248,26 +254,85 @@ function createErrorMetricAttributes(error) {
248
254
  if (error === void 0 || error === null) return {};
249
255
  return { "error.type": getErrorType(error) };
250
256
  }
251
- //#endregion
252
- //#region src/interceptor/http.ts
257
+ // src/telemetry/logs.ts
258
+ const CLIENT_EXCEPTIONS = {
259
+ http: {
260
+ eventName: "http.client.request.exception",
261
+ body: "HTTP client request failed"
262
+ },
263
+ sse: {
264
+ eventName: "defjs.client.sse.exception",
265
+ body: "SSE client connection failed"
266
+ },
267
+ websocket: {
268
+ eventName: "defjs.client.websocket.exception",
269
+ body: "WebSocket client connection failed"
270
+ }
271
+ };
272
+ /**
273
+ * Caller cancellation produces no log. A timeout still logs.
274
+ *
275
+ * The request signal alone is not cancellation: WebSocket `finish()` aborts that same signal
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.
278
+ */
279
+ function isCallerCancellation(request, error) {
280
+ const reason = request.abort?.reason;
281
+ if (isTimeoutCause(error) || isTimeoutCause(reason)) return false;
282
+ return isAbortCause(error) || isAbortCause(reason);
283
+ }
284
+ function emitClientException(logger, details) {
285
+ if (!logger) return;
286
+ try {
287
+ const { error, transport, request } = details;
288
+ if (isCallerCancellation(request, error)) return;
289
+ if (error instanceof Error && "code" in error) {
290
+ if (error.code === "NET_ABORTED" || transport === "http" && error.code === "HTTP_STATUS") return;
291
+ }
292
+ const attributes = {
293
+ ...createServerMetricAttributes(request),
294
+ "defjs.phase": details.phase,
295
+ "exception.type": getErrorType(error)
296
+ };
297
+ if (transport === "http") attributes["http.request.method"] = request.method;
298
+ const closeCode = details.closeInfo?.code;
299
+ if (transport === "sse" && typeof closeCode === "string") attributes["defjs.sse.fault.code"] = closeCode;
300
+ if (transport === "websocket" && typeof closeCode === "number") attributes["websocket.close.code"] = closeCode;
301
+ logger.emit({
302
+ ...CLIENT_EXCEPTIONS[transport],
303
+ context: details.context,
304
+ severityNumber: SeverityNumber.WARN,
305
+ severityText: "WARN",
306
+ attributes
307
+ });
308
+ } catch {
309
+ try {
310
+ diag.error("@defjs/opentelemetry-server: failed to emit client diagnostic log");
311
+ } catch {}
312
+ }
313
+ }
314
+ // src/interceptor/http.ts
253
315
  function createOpenTelemetryHttpInterceptor(options) {
254
316
  return createHttpInterceptor(async (req, next) => {
255
- const { tracer, propagator, metrics, requireParentSpan, startSpanHook, requestHook, responseHook } = options;
256
- if (requireParentSpan && !trace.getActiveSpan()) return next(req);
317
+ const { tracer, logger, propagator, metrics, requireParentSpan, startSpanHook, requestHook, responseHook } = options;
318
+ if (tracer && requireParentSpan && !trace.getActiveSpan()) return next(req);
257
319
  const parentCtx = propagator.extract(context.active(), req.headers ?? new Headers(), headersGetter);
258
320
  const { url, serverAddress, serverPort } = resolveHttpUrl(req.endpoint, req.baseEndpoint);
259
- const startSpanHookResult = resolveStartSpanHook(startSpanHook, req);
260
- const span = createHttpSpan(tracer, req.method, url, parentCtx, req.operation, {
261
- ...serverAddress ? { "server.address": serverAddress } : {},
262
- ...serverPort ? { "server.port": serverPort } : {},
263
- ...startSpanHookResult.ok ? startSpanHookResult.attributes : {}
264
- });
265
- if (!startSpanHookResult.ok) runSpanHook(span, "startSpanHook", () => {
266
- throw startSpanHookResult.error;
267
- });
268
- runSpanHook(span, "requestHook", () => requestHook?.(span, req));
269
- const spanCtx = trace.setSpan(parentCtx, span);
270
- const headers = req.headers instanceof Headers ? req.headers : new Headers(req.headers);
321
+ let span;
322
+ if (tracer) {
323
+ const startSpanHookResult = resolveStartSpanHook(startSpanHook, req);
324
+ span = createHttpSpan(tracer, req.method, url, parentCtx, req.operation, {
325
+ ...serverAddress ? { "server.address": serverAddress } : {},
326
+ ...serverPort ? { "server.port": serverPort } : {},
327
+ ...startSpanHookResult.ok ? startSpanHookResult.attributes : {}
328
+ });
329
+ if (!startSpanHookResult.ok) runSpanHook(span, "startSpanHook", () => {
330
+ throw startSpanHookResult.error;
331
+ });
332
+ }
333
+ if (span) runSpanHook(span, "requestHook", () => requestHook?.(span, req));
334
+ const spanCtx = span ? trace.setSpan(parentCtx, span) : parentCtx;
335
+ const headers = new Headers(req.headers);
271
336
  propagator.inject(spanCtx, headers, headersSetter);
272
337
  const startTime = performance.now();
273
338
  try {
@@ -276,35 +341,46 @@ function createOpenTelemetryHttpInterceptor(options) {
276
341
  headers
277
342
  });
278
343
  const durationS = durationSeconds(startTime);
279
- runSpanHook(span, "responseHook", () => responseHook?.(span, response, req));
280
- setSpanHttpResponse(span, response.status, response.error);
344
+ if (span) {
345
+ runSpanHook(span, "responseHook", () => responseHook?.(span, response, req));
346
+ setSpanHttpResponse(span, response.status);
347
+ }
281
348
  metrics?.requestDuration.record(durationS, createHttpMetricAttributes(req, response));
282
349
  return response;
283
350
  } catch (error) {
284
351
  const durationS = durationSeconds(startTime);
285
- setSpanError(span, error);
352
+ emitClientException(logger, {
353
+ request: req,
354
+ context: spanCtx,
355
+ error,
356
+ transport: "http",
357
+ phase: "request"
358
+ });
359
+ if (span) setSpanError(span, error);
286
360
  metrics?.requestDuration.record(durationS, createHttpMetricAttributes(req, void 0, error));
287
361
  throw error;
288
362
  }
289
363
  });
290
364
  }
291
- //#endregion
292
- //#region src/interceptor/sse.ts
365
+ // src/interceptor/sse.ts
293
366
  function createOpenTelemetrySSEInterceptor(options) {
294
367
  return createSSEInterceptor(async (req, next) => {
295
- const { tracer, propagator, metrics, requireParentSpan, startSpanHook, requestHook, responseHook } = options;
296
- if (requireParentSpan && !trace.getActiveSpan()) return next(req);
368
+ const { tracer, logger, propagator, metrics, requireParentSpan, startSpanHook, requestHook, responseHook } = options;
369
+ if (tracer && requireParentSpan && !trace.getActiveSpan()) return next(req);
297
370
  const parentCtx = propagator.extract(context.active(), req.headers ?? new Headers(), headersGetter);
298
371
  const url = resolveUrl(req.endpoint, req.baseEndpoint);
299
- const startSpanHookResult = resolveStartSpanHook(startSpanHook, req);
300
- const span = createSSESpan(tracer, url, parentCtx, req.operation, startSpanHookResult.ok ? startSpanHookResult.attributes : void 0);
301
- if (!startSpanHookResult.ok) runSpanHook(span, "startSpanHook", () => {
302
- throw startSpanHookResult.error;
303
- });
304
- const spanCtx = trace.setSpan(parentCtx, span);
305
- const headers = req.headers instanceof Headers ? req.headers : new Headers(req.headers);
372
+ let span;
373
+ if (tracer) {
374
+ const startSpanHookResult = resolveStartSpanHook(startSpanHook, req);
375
+ span = createSSESpan(tracer, url, parentCtx, req.operation, startSpanHookResult.ok ? startSpanHookResult.attributes : void 0);
376
+ if (!startSpanHookResult.ok) runSpanHook(span, "startSpanHook", () => {
377
+ throw startSpanHookResult.error;
378
+ });
379
+ }
380
+ const spanCtx = span ? trace.setSpan(parentCtx, span) : parentCtx;
381
+ if (span) runSpanHook(span, "requestHook", () => requestHook?.(span, req));
382
+ const headers = new Headers(req.headers);
306
383
  propagator.inject(spanCtx, headers, headersSetter);
307
- runSpanHook(span, "requestHook", () => requestHook?.(span, req));
308
384
  const connectStartMs = performance.now();
309
385
  try {
310
386
  const stream = await next({
@@ -313,8 +389,10 @@ function createOpenTelemetrySSEInterceptor(options) {
313
389
  });
314
390
  const connectedAtMs = performance.now();
315
391
  const activeAttributes = createServerMetricAttributes(req);
316
- runSpanHook(span, "responseHook", () => responseHook?.(span, stream, req));
317
- addSpanEvent(span, "sse.connected");
392
+ if (span) {
393
+ runSpanHook(span, "responseHook", () => responseHook?.(span, stream, req));
394
+ addSpanEvent(span, "sse.connected");
395
+ }
318
396
  metrics?.connectDuration.record(durationSeconds(connectStartMs, connectedAtMs), createConnectionMetricAttributes(req, "success"));
319
397
  metrics?.activeStreams.add(1, activeAttributes);
320
398
  const closeActiveStream = () => {
@@ -323,59 +401,82 @@ function createOpenTelemetrySSEInterceptor(options) {
323
401
  stream.closed.then((closeInfo) => {
324
402
  closeActiveStream();
325
403
  const closeAttributes = {
326
- "sse.close.code": closeInfo.code,
327
- ...closeInfo.code === "error" ? { "defjs.sse.error.code": closeInfo.errorCode } : {}
404
+ "sse.close.kind": closeInfo.kind,
405
+ ...closeInfo.kind === "error" ? { "defjs.sse.fault.code": closeInfo.code } : {}
328
406
  };
329
- if (closeInfo.code === "error") {
330
- addSpanEvent(span, "sse.error", closeAttributes);
407
+ if (closeInfo.kind === "error") {
408
+ emitClientException(logger, {
409
+ request: req,
410
+ context: spanCtx,
411
+ error: closeInfo.cause,
412
+ transport: "sse",
413
+ phase: "connection",
414
+ closeInfo
415
+ });
416
+ if (span) addSpanEvent(span, "sse.error", closeAttributes);
331
417
  metrics?.connectionDuration.record(durationSeconds(connectedAtMs), createConnectionMetricAttributes(req, "error", {
332
418
  ...closeAttributes,
333
419
  ...createErrorMetricAttributes(closeInfo.cause)
334
420
  }));
335
- setSpanError(span, closeInfo.cause);
421
+ if (span) setSpanError(span, closeInfo.cause);
336
422
  return;
337
423
  }
338
- addSpanEvent(span, closeInfo.code === "aborted" ? "sse.aborted" : "sse.closed", closeAttributes);
424
+ if (span) addSpanEvent(span, closeInfo.kind === "aborted" ? "sse.aborted" : "sse.closed", closeAttributes);
339
425
  metrics?.connectionDuration.record(durationSeconds(connectedAtMs), createConnectionMetricAttributes(req, "success", closeAttributes));
340
- endSpan(span);
426
+ if (span) endSpan(span);
341
427
  }, (error) => {
342
428
  closeActiveStream();
343
- addSpanEvent(span, "sse.error", createErrorMetricAttributes(error));
429
+ emitClientException(logger, {
430
+ request: req,
431
+ context: spanCtx,
432
+ error,
433
+ transport: "sse",
434
+ phase: "connection"
435
+ });
436
+ if (span) addSpanEvent(span, "sse.error", createErrorMetricAttributes(error));
344
437
  metrics?.connectionDuration.record(durationSeconds(connectedAtMs), createConnectionMetricAttributes(req, "error", createErrorMetricAttributes(error)));
345
- setSpanError(span, error);
438
+ if (span) setSpanError(span, error);
346
439
  });
347
440
  return stream;
348
441
  } catch (error) {
442
+ emitClientException(logger, {
443
+ request: req,
444
+ context: spanCtx,
445
+ error,
446
+ transport: "sse",
447
+ phase: "connect"
448
+ });
349
449
  metrics?.connectDuration.record(durationSeconds(connectStartMs), createConnectionMetricAttributes(req, "error", createErrorMetricAttributes(error)));
350
- setSpanError(span, error);
450
+ if (span) setSpanError(span, error);
351
451
  throw error;
352
452
  }
353
453
  });
354
454
  }
355
- //#endregion
356
- //#region src/interceptor/web_socket.ts
455
+ // src/interceptor/web_socket.ts
357
456
  function createOpenTelemetryWebSocketInterceptor(options) {
358
457
  return createWebSocketInterceptor(async (req, next) => {
359
- const { tracer, propagator, metrics, requireParentSpan, queryPropagation = false, startSpanHook, requestHook, responseHook } = options;
360
- if (requireParentSpan && !trace.getActiveSpan()) return next(req);
458
+ const { tracer, logger, propagator, metrics, requireParentSpan, queryPropagation = false, startSpanHook, requestHook, responseHook } = options;
459
+ if (tracer && requireParentSpan && !trace.getActiveSpan()) return next(req);
361
460
  const url = resolveUrl(req.endpoint, req.baseEndpoint);
362
461
  const parentCtx = propagator.extract(context.active(), req.headers ?? new Headers(), headersGetter);
363
- const startSpanHookResult = resolveStartSpanHook(startSpanHook, req);
364
- const span = createWebSocketSpan(tracer, url, parentCtx, req.operation, startSpanHookResult.ok ? startSpanHookResult.attributes : void 0);
365
- if (!startSpanHookResult.ok) runSpanHook(span, "startSpanHook", () => {
366
- throw startSpanHookResult.error;
367
- });
368
- const spanCtx = trace.setSpan(parentCtx, span);
462
+ let span;
463
+ if (tracer) {
464
+ const startSpanHookResult = resolveStartSpanHook(startSpanHook, req);
465
+ span = createWebSocketSpan(tracer, url, parentCtx, req.operation, startSpanHookResult.ok ? startSpanHookResult.attributes : void 0);
466
+ if (!startSpanHookResult.ok) runSpanHook(span, "startSpanHook", () => {
467
+ throw startSpanHookResult.error;
468
+ });
469
+ }
470
+ const spanCtx = span ? trace.setSpan(parentCtx, span) : parentCtx;
369
471
  let queryParams = req.queryParams;
370
472
  let queryString = req.queryString;
371
473
  if (queryPropagation) {
372
- const originalParams = new URLSearchParams(queryParams);
373
- const carrier = { params: new URLSearchParams(queryParams) };
474
+ const carrier = { params: new URLSearchParams() };
374
475
  propagator.inject(spanCtx, carrier, queryStringSetter);
375
- queryParams = carrier.params;
376
- queryString = appendInjectedQueryString(queryString, originalParams, carrier.params);
476
+ queryString = replaceInjectedPairs(queryString ?? new URLSearchParams(queryParams).toString(), carrier.params);
477
+ queryParams = new URLSearchParams(queryString);
377
478
  }
378
- runSpanHook(span, "requestHook", () => requestHook?.(span, req));
479
+ if (span) runSpanHook(span, "requestHook", () => requestHook?.(span, req));
379
480
  const connectStartMs = performance.now();
380
481
  try {
381
482
  const session = await next({
@@ -385,8 +486,10 @@ function createOpenTelemetryWebSocketInterceptor(options) {
385
486
  });
386
487
  const connectedAtMs = performance.now();
387
488
  const activeAttributes = createServerMetricAttributes(req);
388
- runSpanHook(span, "responseHook", () => responseHook?.(span, session, req));
389
- addSpanEvent(span, "websocket.connected");
489
+ if (span) {
490
+ runSpanHook(span, "responseHook", () => responseHook?.(span, session, req));
491
+ addSpanEvent(span, "websocket.connected");
492
+ }
390
493
  metrics?.connectDuration.record(durationSeconds(connectStartMs, connectedAtMs), createConnectionMetricAttributes(req, "success"));
391
494
  metrics?.activeConnections.add(1, activeAttributes);
392
495
  const closeActiveConnection = () => {
@@ -399,63 +502,88 @@ function createOpenTelemetryWebSocketInterceptor(options) {
399
502
  const closeCause = runtimeCloseInfo.cause;
400
503
  if (typeof closeKind === "string" ? closeKind !== "closed" : typeof closeCause !== "undefined") {
401
504
  const error = typeof closeKind === "string" ? closeKind === "error" ? closeCause : closeCause ?? ERR_ABORTED : closeCause;
402
- addSpanEvent(span, "websocket.error", createErrorMetricAttributes(error));
505
+ if (closeKind !== "aborted") emitClientException(logger, {
506
+ request: req,
507
+ context: spanCtx,
508
+ error,
509
+ transport: "websocket",
510
+ phase: "connection",
511
+ closeInfo: runtimeCloseInfo
512
+ });
513
+ if (span) addSpanEvent(span, "websocket.error", createErrorMetricAttributes(error));
403
514
  metrics?.connectionDuration.record(durationSeconds(connectedAtMs), createConnectionMetricAttributes(req, "error", createErrorMetricAttributes(error)));
404
- setSpanError(span, error);
515
+ if (span) setSpanError(span, error);
405
516
  return;
406
517
  }
407
- addSpanEvent(span, "websocket.closed");
518
+ if (span) addSpanEvent(span, "websocket.closed");
408
519
  metrics?.connectionDuration.record(durationSeconds(connectedAtMs), createConnectionMetricAttributes(req, "success"));
409
- endSpan(span);
520
+ if (span) endSpan(span);
410
521
  }, (error) => {
411
522
  closeActiveConnection();
412
- addSpanEvent(span, "websocket.error", createErrorMetricAttributes(error));
523
+ emitClientException(logger, {
524
+ request: req,
525
+ context: spanCtx,
526
+ error,
527
+ transport: "websocket",
528
+ phase: "connection"
529
+ });
530
+ if (span) addSpanEvent(span, "websocket.error", createErrorMetricAttributes(error));
413
531
  metrics?.connectionDuration.record(durationSeconds(connectedAtMs), createConnectionMetricAttributes(req, "error", createErrorMetricAttributes(error)));
414
- setSpanError(span, error);
532
+ if (span) setSpanError(span, error);
415
533
  });
416
534
  return session;
417
535
  } catch (error) {
536
+ emitClientException(logger, {
537
+ request: req,
538
+ context: spanCtx,
539
+ error,
540
+ transport: "websocket",
541
+ phase: "connect"
542
+ });
418
543
  metrics?.connectDuration.record(durationSeconds(connectStartMs), createConnectionMetricAttributes(req, "error", createErrorMetricAttributes(error)));
419
- setSpanError(span, error);
544
+ if (span) setSpanError(span, error);
420
545
  throw error;
421
546
  }
422
547
  });
423
548
  }
424
- function appendInjectedQueryString(queryString, originalParams, nextParams) {
425
- const injectedParams = new URLSearchParams();
426
- nextParams.forEach((value, key) => {
427
- if (originalParams.getAll(key).includes(value)) return;
428
- injectedParams.append(key, value);
429
- });
430
- const injectedQueryString = injectedParams.toString();
431
- if (!injectedQueryString) return queryString ?? "";
432
- if (!queryString) return injectedQueryString;
433
- return `${queryString}&${injectedQueryString}`;
549
+ /**
550
+ * Put the injected pairs last and drop earlier pairs of the same name, so a receiver reading the first value gets the injected one.
551
+ * Every other pair keeps the bytes it was serialized with.
552
+ */
553
+ function replaceInjectedPairs(queryString, injected) {
554
+ const added = injected.toString();
555
+ if (!added) return queryString;
556
+ const prefixed = queryString.startsWith("?");
557
+ const query = prefixed ? queryString.slice(1) : queryString;
558
+ const combined = [...query ? query.split("&").filter((pair) => !injected.has(new URLSearchParams(`&${pair}`).keys().next().value ?? "")) : [], added].join("&");
559
+ return prefixed || combined.startsWith("?") ? `?${combined}` : combined;
434
560
  }
435
- //#endregion
436
- //#region src/option.ts
561
+ // src/option.ts
437
562
  /**
438
563
  * Client option that installs outbound OpenTelemetry interceptors for HTTP, SSE, and WebSocket.
439
564
  *
440
- * Does not initialize the OpenTelemetry SDK — supply a `tracer` (and optional `meter`) from your app.
441
- * Each transport can be toggled or customized via `http`, `sse`, and `webSocket`.
565
+ * Appends one interceptor per enabled transport, in the order HTTP, SSE, WebSocket. Apply it at `createClient` time. It does not initialize the OpenTelemetry SDK or acquire global tracer, meter, or logger handles: pass the ones your application owns. Each transport can be toggled or customized via `http`, `sse`, and `webSocket`.
566
+ *
567
+ * `options` is required. It carries `tracer`, `meter`, `logger`, `propagator`, `requireParentSpan`, and the `http` / `sse` / `webSocket` transport options. Pass `{}` to install the interceptors with propagation only.
568
+ *
569
+ * Each of `tracer`, `meter` and `logger` is optional and works without the others. Without `tracer`, the plugin creates no local spans, never calls `trace.getTracer(...)`, and skips `startSpanHook`, `requestHook` and `responseHook`. Propagation still forwards the extracted parent context.
570
+ *
571
+ * HTTP, SSE, and WebSocket are on by default. Pass `{ enabled: false }` on a transport to skip it.
442
572
  *
443
- * @param options - Tracer, optional meter/propagator, and per-transport settings.
573
+ * @param options - Required settings object. Each of `tracer`, `meter`, `logger`, and `propagator` may be omitted; pass `{}` for propagation only.
444
574
  * @returns A `ClientOption` for `createClient(...)`.
445
575
  *
446
- * @example
447
- * ```ts
448
- * const client = createClient(
449
- * withEndpoint('https://api.example.com'),
450
- * withOpenTelemetryServer({ tracer }),
451
- * )
452
- * ```
576
+ * @group opentelemetry-server
577
+ * @see {@link ClientOption}
578
+ * @see {@link OpenTelemetryServerOptions}
579
+ * @exampleFile examples/api/with-opentelemetry-server.ts#example
453
580
  */
454
581
  function withOpenTelemetryServer(options) {
455
- const { tracer, meter, propagator = new CompositePropagator({ propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()] }), requireParentSpan = false } = options;
582
+ const { tracer, meter, logger, propagator = new CompositePropagator({ propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()] }), requireParentSpan = false } = options;
456
583
  return (config) => {
457
584
  if (isTransportEnabled(options.http)) config.interceptors.push(createOpenTelemetryHttpInterceptor({
458
585
  tracer,
586
+ logger,
459
587
  propagator,
460
588
  metrics: meter ? createHttpClientMetrics(meter) : void 0,
461
589
  requireParentSpan,
@@ -465,6 +593,7 @@ function withOpenTelemetryServer(options) {
465
593
  }));
466
594
  if (isTransportEnabled(options.sse)) config.interceptors.push(createOpenTelemetrySSEInterceptor({
467
595
  tracer,
596
+ logger,
468
597
  propagator,
469
598
  metrics: meter ? createSSEClientMetrics(meter) : void 0,
470
599
  requireParentSpan,
@@ -474,6 +603,7 @@ function withOpenTelemetryServer(options) {
474
603
  }));
475
604
  if (isTransportEnabled(options.webSocket)) config.interceptors.push(createOpenTelemetryWebSocketInterceptor({
476
605
  tracer,
606
+ logger,
477
607
  propagator,
478
608
  metrics: meter ? createWebSocketClientMetrics(meter) : void 0,
479
609
  requireParentSpan,
@@ -487,5 +617,4 @@ function withOpenTelemetryServer(options) {
487
617
  function isTransportEnabled(option) {
488
618
  return option?.enabled !== false;
489
619
  }
490
- //#endregion
491
620
  export { withOpenTelemetryServer };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@defjs/opentelemetry-server",
3
- "version": "0.0.1-beta.0",
3
+ "version": "0.0.1",
4
+ "description": "Append OpenTelemetry interceptors to an application-owned Defjs client.",
4
5
  "bugs": {
5
6
  "url": "https://github.com/defjs/defjs/issues"
6
7
  },
@@ -12,10 +13,13 @@
12
13
  "type": "module",
13
14
  "main": "./index.js",
14
15
  "types": "./index.d.ts",
16
+ "dependencies": {
17
+ "@opentelemetry/api-logs": "^0.223.0"
18
+ },
15
19
  "peerDependencies": {
16
- "@defjs/core": "0.4.0-beta.0",
20
+ "@defjs/core": "^0.4.0",
17
21
  "@opentelemetry/api": "^1.0.0",
18
- "@opentelemetry/core": "^2.0.0"
22
+ "@opentelemetry/core": "^2.0.0 || ^3.0.0"
19
23
  },
20
24
  "module": "./index.js",
21
25
  "typings": "./index.d.ts",