@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.
- package/README.md +165 -66
- package/index.d.ts +103 -18
- package/index.js +238 -109
- 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** —
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
import { withOpenTelemetryServer } from
|
|
31
|
-
import { trace } from
|
|
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(
|
|
36
|
+
const tracer = trace.getTracer("my-service");
|
|
35
37
|
|
|
36
38
|
const client = createClient(
|
|
37
|
-
withEndpoint(
|
|
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:
|
|
51
|
-
operation:
|
|
52
|
-
path:
|
|
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(
|
|
155
|
+
withEndpoint("https://api.example.com"),
|
|
66
156
|
withOpenTelemetryServer({
|
|
67
|
-
tracer, //
|
|
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 {
|
|
165
|
+
return { "app.operation": req.operation ?? "unclassified" };
|
|
75
166
|
},
|
|
76
167
|
responseHook(span, res, req) {
|
|
77
|
-
span.setAttribute(
|
|
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
|
|
113
|
-
| ------------------- |
|
|
114
|
-
| `tracer` | `Tracer`
|
|
115
|
-
| `meter` | `Meter`
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
186
|
-
return {
|
|
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
|
|
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`.
|
|
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
|
|
305
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
210
306
|
|
|
211
|
-
const outcome = await tracer.startActiveSpan(
|
|
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(
|
|
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(
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
|
288
|
-
- [Client](https://defjs.
|
|
289
|
-
- [SSE](https://defjs.
|
|
290
|
-
- [WebSocket](https://defjs.
|
|
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
|
-
|
|
4
|
-
/**
|
|
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
|
|
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
|
-
/**
|
|
32
|
-
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
|
-
/**
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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 -
|
|
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
|
-
* @
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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,
|
|
3
|
-
import { SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
|
|
4
|
-
|
|
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.
|
|
20
|
+
if (carrier && key && value !== void 0) carrier.params.set(key, String(value));
|
|
20
21
|
} };
|
|
21
|
-
|
|
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
|
|
72
|
+
function setSpanHttpResponse(span, status) {
|
|
73
73
|
if (status !== 0) span.setAttribute("http.response.status_code", status);
|
|
74
|
-
const errorType = getHttpResponseErrorType(status
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
252
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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.
|
|
327
|
-
...closeInfo.
|
|
404
|
+
"sse.close.kind": closeInfo.kind,
|
|
405
|
+
...closeInfo.kind === "error" ? { "defjs.sse.fault.code": closeInfo.code } : {}
|
|
328
406
|
};
|
|
329
|
-
if (closeInfo.
|
|
330
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
373
|
-
const carrier = { params: new URLSearchParams(queryParams) };
|
|
474
|
+
const carrier = { params: new URLSearchParams() };
|
|
374
475
|
propagator.inject(spanCtx, carrier, queryStringSetter);
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
441
|
-
*
|
|
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 -
|
|
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
|
-
* @
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
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
|
|
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
|
|
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",
|