@photon-ai/otel 1.1.0 → 2.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 +75 -19
- package/dist/index.d.ts +111 -58
- package/dist/index.js +516 -497
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -7,10 +7,10 @@ Vanilla OTel works, but the setup is verbose, the logger plumbing is awkward, an
|
|
|
7
7
|
- **`setupOtel()`** — idempotent one-call bootstrap for traces + logs. Honors all standard `OTEL_EXPORTER_OTLP_*` env vars.
|
|
8
8
|
- **`createLogger(module)`** — structured logger that writes to both the OTel logger provider and `console`, with automatic trace correlation and exception capture. Every level (`debug`/`info`/`warn`/`error`) accepts `attrs` **and** an `error`, and is gated by a configurable `LOG_LEVEL`.
|
|
9
9
|
- **`withSpan(name, attrs?, fn)`** — wrap any sync or async function in a span; errors are recorded and PII in the error message is scrubbed before being attached to span status.
|
|
10
|
-
- **Automatic `fetch` tracing** — `setupOtel()`
|
|
10
|
+
- **Automatic `fetch` tracing** — `setupOtel()` instruments outbound `fetch` so every request gets a CLIENT span and W3C trace-context headers. On **Node** it uses the official `@opentelemetry/instrumentation-undici`; on **Bun** — whose native fetch emits nothing for the standard `diagnostics_channel`-based instrumentations — it wraps `globalThis.fetch`. Pass `instrumentFetch: { mode: "global" }` to force the wrap on both for identical spans.
|
|
11
11
|
- **`sanitizeEmail` / `sanitizePhone` / `sanitizeErrorMessage`** — PII helpers you can reuse anywhere.
|
|
12
12
|
|
|
13
|
-
OTLP/HTTP only (no gRPC, no proto).
|
|
13
|
+
OTLP/HTTP only (no gRPC, no proto). Runs on Bun and Node ≥ 20.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -49,7 +49,8 @@ If `OTEL_EXPORTER_OTLP_ENDPOINT` (or the `endpoint` option) is unset, `setupOtel
|
|
|
49
49
|
| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
50
50
|
| `setupOtel(options): OtelHandle` | Boots OTLP/HTTP traces + logs. Idempotent. Returns `{ shutdown(): Promise<void> }`. |
|
|
51
51
|
| `isOtelActive(): boolean` | Returns `true` if `setupOtel` has already run in this process. |
|
|
52
|
-
| `instrumentFetch(options?): FetchInstrumentation` |
|
|
52
|
+
| `instrumentFetch(options?): FetchInstrumentation` | Low-level wrap of `globalThis.fetch` for CLIENT spans + W3C propagation. Returns `{ unpatch() }`. `setupOtel` calls this on Bun; on Node it prefers native undici. |
|
|
53
|
+
| `createInstrumentedFetch(baseFetch?, options?): typeof fetch` | Returns a NEW instrumented fetch (CLIENT spans + W3C propagation) wrapping `baseFetch` (default `globalThis.fetch`) without touching the global. For SDKs that take a `fetch` option. |
|
|
53
54
|
| `createLogger(module): PhotonLogger` | Returns `{ info, warn, error, debug }`. Each call emits to OTel + `console`, correlates to active span. |
|
|
54
55
|
| `setLogLevel(level): void` | Set the minimum level emitted (`debug`/`info`/`warn`/`error`/`silent`). `LOG_LEVEL` env still wins. |
|
|
55
56
|
| `getLogLevel(): LogLevel` | Current effective level after env / override / default resolution. |
|
|
@@ -115,28 +116,81 @@ Standard OpenTelemetry env vars always take precedence over `SetupOtelOptions`:
|
|
|
115
116
|
|
|
116
117
|
## Automatic fetch instrumentation
|
|
117
118
|
|
|
118
|
-
`setupOtel()`
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`server.port`, and `http.response.status_code
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
119
|
+
`setupOtel()` instruments outbound `fetch` to emit a CLIENT span per request, carrying W3C
|
|
120
|
+
`traceparent` (and baggage) headers so traces continue across services. Spans are named by HTTP
|
|
121
|
+
method (`GET`, `POST`, …) and carry `http.request.method`, `url.full`, `server.address`,
|
|
122
|
+
`server.port`, and `http.response.status_code`. This covers **outbound** requests only — inbound
|
|
123
|
+
`Bun.serve`/Elysia server spans are separate (see [Framework integration](#framework-integration)).
|
|
124
|
+
|
|
125
|
+
The strategy depends on the runtime (`mode: "auto"`, the default):
|
|
126
|
+
|
|
127
|
+
- **Node** uses the official `@opentelemetry/instrumentation-undici` (Node's `fetch` is undici-backed).
|
|
128
|
+
It captures all undici traffic — not just `globalThis.fetch` — emits the full HTTP-client semantic
|
|
129
|
+
conventions (`url.scheme`, `url.path`, `network.peer.*`, `user_agent.original`, …), and never
|
|
130
|
+
monkey-patches the global. It ships as an optional dependency; if it isn't installed, the library
|
|
131
|
+
falls back to the global wrap.
|
|
132
|
+
- **Bun** wraps `globalThis.fetch` directly. Bun's native `fetch` emits no `diagnostics_channel`
|
|
133
|
+
events, so `@opentelemetry/instrumentation-undici` / `-http` produce no spans there — wrapping the
|
|
134
|
+
global is the only mechanism that works.
|
|
135
|
+
|
|
136
|
+
Options (`instrumentFetch`):
|
|
137
|
+
|
|
138
|
+
- **`true` / `false`:** force on (even without an endpoint) / off. Defaults to on when a traces
|
|
139
|
+
endpoint is configured.
|
|
140
|
+
- **`mode`:** `"auto"` (default — native on Node, wrap on Bun) or `"global"` (wrap on both runtimes).
|
|
141
|
+
Choose `"global"` when you want identical spans everywhere and the built-in PII scrubbing of error
|
|
142
|
+
messages kept on Node (see caveats).
|
|
143
|
+
- **`ignore`:** `instrumentFetch: { ignore: (url) => url.includes("/healthz") }`. Your own OTLP
|
|
128
144
|
endpoint is always excluded automatically, so the exporter never traces itself.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
|
|
146
|
+
Caveats:
|
|
147
|
+
|
|
148
|
+
- **Telemetry differs by runtime under `"auto"`.** undici (Node) emits richer attributes and follows
|
|
149
|
+
HTTP semconv for span status — a 2xx client span is left `UNSET`, and only `5xx`/network failures are
|
|
150
|
+
marked `ERROR`; the Bun wrap marks all `4xx`/`5xx` as `ERROR`. The Bun wrap also scrubs PII from the
|
|
151
|
+
error message attached to span status — **undici does not**. Use `mode: "global"` for parity.
|
|
152
|
+
- **`url.full` includes the query string** on both. If your URLs carry secrets there, use `ignore` or
|
|
153
|
+
redact upstream.
|
|
154
|
+
- **Native fetch tracing needs Node ≥ 20.6** (the undici instrumentation's floor); older 20.x falls
|
|
155
|
+
back to the global wrap.
|
|
135
156
|
|
|
136
157
|
`setupOtel()` also installs a global W3C trace-context + baggage propagator (previously none was
|
|
137
158
|
registered) — that is what makes the outbound `traceparent` injection, and any manual propagation,
|
|
138
159
|
actually take effect.
|
|
139
160
|
|
|
161
|
+
## Instrumenting a specific fetch (SDKs)
|
|
162
|
+
|
|
163
|
+
Sometimes you don't want to touch `globalThis.fetch` — you just want one SDK's outbound calls traced.
|
|
164
|
+
`createInstrumentedFetch(baseFetch?, options?)` returns a **new** fetch (CLIENT spans + W3C
|
|
165
|
+
`traceparent` injection) wrapping `baseFetch` (default `globalThis.fetch`, read at call time) **without
|
|
166
|
+
mutating the global**. Pass it wherever an SDK accepts a `fetch`:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { createInstrumentedFetch } from "@photon-ai/otel";
|
|
170
|
+
import OpenAI from "openai";
|
|
171
|
+
|
|
172
|
+
const client = new OpenAI({
|
|
173
|
+
// tag every span from this SDK so it's distinguishable from other traffic
|
|
174
|
+
fetch: createInstrumentedFetch(undefined, {
|
|
175
|
+
attributes: { "peer.service": "openai" },
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- Returns a fetch function directly — there's no global lifecycle, so no `unpatch()` handle.
|
|
181
|
+
- Idempotent: passing an already-instrumented fetch returns it unchanged.
|
|
182
|
+
- `options`: `ignore: (url) => boolean` skips spans for some URLs; `attributes` merges static attributes
|
|
183
|
+
into every span (the practical way to tell different SDKs' spans apart).
|
|
184
|
+
- Always uses the wrapper technique, so it behaves identically on Bun and Node (the native undici
|
|
185
|
+
instrumentation can't target a single instance).
|
|
186
|
+
|
|
187
|
+
**Avoid double-counting on Node.** If `setupOtel()`'s global fetch instrumentation is active (the
|
|
188
|
+
default on Node uses undici, which captures *all* undici traffic), an SDK request made through a
|
|
189
|
+
per-instance wrapper is recorded **twice** — once by the wrapper, once by undici. When instrumenting
|
|
190
|
+
SDKs per-instance, disable the global path with `setupOtel({ instrumentFetch: false })` (or reserve
|
|
191
|
+
per-instance wrapping for SDKs you accept doubling on). **Bun has no such issue** — its global wrap only
|
|
192
|
+
affects `globalThis.fetch`, so a separately-passed instrumented fetch is counted once.
|
|
193
|
+
|
|
140
194
|
## Running on Node vs Bun
|
|
141
195
|
|
|
142
196
|
The same code runs unmodified on both. Pick whichever you prefer:
|
|
@@ -149,6 +203,8 @@ node --experimental-strip-types src/server.ts
|
|
|
149
203
|
|
|
150
204
|
The package uses `process.env` (not `Bun.env`) and `AsyncLocalStorageContextManager` (backed by `async_hooks`), both of which are supported natively by Bun and Node ≥ 20.
|
|
151
205
|
|
|
206
|
+
The one runtime-specific behavior is fetch instrumentation — native undici on Node, a `globalThis.fetch` wrap on Bun (see [Automatic fetch instrumentation](#automatic-fetch-instrumentation)). Set `instrumentFetch: { mode: "global" }` for identical behavior on both.
|
|
207
|
+
|
|
152
208
|
For Bun consumers, the `exports` map points at the TypeScript source via the `bun` condition for faster cold starts during dev. Node consumers get the pre-built ESM bundle.
|
|
153
209
|
|
|
154
210
|
## Why HTTP exporters only?
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,62 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { Attributes } from "@opentelemetry/api";
|
|
2
|
+
|
|
3
|
+
//#region src/instrument-fetch.d.ts
|
|
4
|
+
interface FetchSpanOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Static attributes merged into every CLIENT span this instrumentation
|
|
7
|
+
* produces. Handy for tagging an SDK's traffic, e.g. `{ "peer.service":
|
|
8
|
+
* "openai" }`, so spans from different instrumented fetches stay
|
|
9
|
+
* distinguishable.
|
|
10
|
+
*/
|
|
11
|
+
attributes?: Attributes;
|
|
12
|
+
/**
|
|
13
|
+
* Return `true` to skip instrumenting a request whose absolute URL is passed
|
|
14
|
+
* in. Useful to drop noisy endpoints or URLs that carry secrets in their
|
|
15
|
+
* query string. The request is still performed — only the span is skipped.
|
|
16
|
+
*/
|
|
17
|
+
ignore?: (url: string) => boolean;
|
|
18
|
+
}
|
|
19
|
+
interface InstrumentFetchOptions extends FetchSpanOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Which fetch-instrumentation strategy `setupOtel()` should use:
|
|
22
|
+
* - `"auto"` (default): the official `@opentelemetry/instrumentation-undici`
|
|
23
|
+
* on Node (richer HTTP-client semantic conventions, captures all undici
|
|
24
|
+
* traffic, no global monkey-patch), and the `globalThis.fetch` wrap on Bun
|
|
25
|
+
* (the only thing that works there).
|
|
26
|
+
* - `"global"`: always wrap `globalThis.fetch` on both runtimes. Produces
|
|
27
|
+
* identical spans everywhere and keeps the built-in PII scrubbing of error
|
|
28
|
+
* messages, at the cost of the richer Node attributes. Use this when you
|
|
29
|
+
* want Bun and Node telemetry to match exactly.
|
|
30
|
+
*
|
|
31
|
+
* Only consulted by `setupOtel()`. Calling `instrumentFetch()` directly always
|
|
32
|
+
* performs a global wrap regardless of this field.
|
|
33
|
+
*/
|
|
34
|
+
mode?: "auto" | "global";
|
|
8
35
|
}
|
|
9
36
|
interface FetchInstrumentation {
|
|
10
|
-
|
|
11
|
-
|
|
37
|
+
/** Restore the original `globalThis.fetch`. Safe to call more than once. */
|
|
38
|
+
unpatch(): void;
|
|
12
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Wrap a single fetch function — not `globalThis.fetch` — so requests made
|
|
42
|
+
* through the RETURNED fetch produce a CLIENT span and carry W3C trace context
|
|
43
|
+
* to the downstream service.
|
|
44
|
+
*
|
|
45
|
+
* Built for SDKs that accept a `fetch` option, e.g.
|
|
46
|
+
* `new OpenAI({ fetch: createInstrumentedFetch() })`. Unlike `instrumentFetch`,
|
|
47
|
+
* it never mutates the global and has no lifecycle to unpatch — it just returns
|
|
48
|
+
* a new fetch you pass where you need it.
|
|
49
|
+
*
|
|
50
|
+
* `baseFetch` defaults to the current `globalThis.fetch`, read at call time.
|
|
51
|
+
* Idempotent: passing an already-instrumented fetch returns it unchanged.
|
|
52
|
+
*
|
|
53
|
+
* Always uses the global-wrap technique (the native undici instrumentation
|
|
54
|
+
* cannot target a single instance), so it behaves identically on Bun and Node.
|
|
55
|
+
* On Node, if `setupOtel`'s global fetch instrumentation is also active, the
|
|
56
|
+
* SDK's request is captured twice — disable it (`instrumentFetch: false`) for
|
|
57
|
+
* paths you instrument per-instance.
|
|
58
|
+
*/
|
|
59
|
+
declare function createInstrumentedFetch(baseFetch?: typeof fetch, options?: FetchSpanOptions): typeof fetch;
|
|
13
60
|
/**
|
|
14
61
|
* Wrap `globalThis.fetch` so every outbound request produces a CLIENT span and
|
|
15
62
|
* carries W3C trace context to the downstream service.
|
|
@@ -24,7 +71,8 @@ interface FetchInstrumentation {
|
|
|
24
71
|
* whose `unpatch()` restores the original fetch.
|
|
25
72
|
*/
|
|
26
73
|
declare function instrumentFetch(options?: InstrumentFetchOptions): FetchInstrumentation;
|
|
27
|
-
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/logger.d.ts
|
|
28
76
|
type LogAttrs = Record<string, string | number | boolean | undefined>;
|
|
29
77
|
/**
|
|
30
78
|
* Minimum severity that gets emitted (to both the OTLP record and the console).
|
|
@@ -39,13 +87,14 @@ declare function setLogLevel(level: LogLevel): void;
|
|
|
39
87
|
/** Current effective log level, after env / override / default resolution. */
|
|
40
88
|
declare function getLogLevel(): LogLevel;
|
|
41
89
|
interface PhotonLogger {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
90
|
+
debug(message: string, attrs?: LogAttrs, error?: unknown): void;
|
|
91
|
+
error(message: string, attrs?: LogAttrs, error?: unknown): void;
|
|
92
|
+
info(message: string, attrs?: LogAttrs, error?: unknown): void;
|
|
93
|
+
warn(message: string, attrs?: LogAttrs, error?: unknown): void;
|
|
46
94
|
}
|
|
47
95
|
declare function createLogger(module: string): PhotonLogger;
|
|
48
|
-
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/sanitize.d.ts
|
|
49
98
|
/**
|
|
50
99
|
* Mask a phone number, keeping the leading `+` (if any) plus the first 3 digits
|
|
51
100
|
* and the last 4 digits visible. Example: `+13315553374` -> `+133xxxxx3374`.
|
|
@@ -66,47 +115,48 @@ declare function sanitizeEmail(input: string): string;
|
|
|
66
115
|
* them to span status.
|
|
67
116
|
*/
|
|
68
117
|
declare function sanitizeErrorMessage(input: string): string;
|
|
69
|
-
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/setup.d.ts
|
|
70
120
|
interface SetupOtelOptions {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Default OTLP/HTTP base endpoint (e.g. `https://otel.example.com`). The
|
|
123
|
+
* `/v1/traces` and `/v1/logs` paths are appended automatically. Standard
|
|
124
|
+
* `OTEL_EXPORTER_OTLP_*` env vars always take precedence.
|
|
125
|
+
*/
|
|
126
|
+
endpoint?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Default OTLP headers (e.g. `{ Authorization: "Basic ..." }`). Merged with
|
|
129
|
+
* any headers parsed from `OTEL_EXPORTER_OTLP_HEADERS`; env values win on
|
|
130
|
+
* conflicts.
|
|
131
|
+
*/
|
|
132
|
+
headers?: Record<string, string>;
|
|
133
|
+
/**
|
|
134
|
+
* Auto-instrument outbound `globalThis.fetch` with CLIENT spans and W3C
|
|
135
|
+
* trace-context propagation. On Bun this is the only fetch instrumentation
|
|
136
|
+
* that works (diagnostics_channel-based instrumentations emit nothing on
|
|
137
|
+
* Bun's native fetch); it works identically on Node.
|
|
138
|
+
*
|
|
139
|
+
* `true` enables with defaults; pass an object to filter URLs via `ignore`.
|
|
140
|
+
* Defaults to enabled when a traces endpoint is configured. Pass `false` to
|
|
141
|
+
* disable.
|
|
142
|
+
*/
|
|
143
|
+
instrumentFetch?: boolean | InstrumentFetchOptions;
|
|
144
|
+
/**
|
|
145
|
+
* Minimum log level emitted by `createLogger()` (to both OTLP and console).
|
|
146
|
+
* The `LOG_LEVEL` env var still takes precedence. Defaults to `debug` in
|
|
147
|
+
* development and `info` otherwise.
|
|
148
|
+
*/
|
|
149
|
+
logLevel?: LogLevel;
|
|
150
|
+
/**
|
|
151
|
+
* Extra resource attributes attached to every span/log alongside
|
|
152
|
+
* `service.name` / `service.version`.
|
|
153
|
+
*/
|
|
154
|
+
resourceAttributes?: Record<string, string | number | boolean>;
|
|
155
|
+
serviceName: string;
|
|
156
|
+
serviceVersion?: string;
|
|
107
157
|
}
|
|
108
158
|
interface OtelHandle {
|
|
109
|
-
|
|
159
|
+
shutdown(): Promise<void>;
|
|
110
160
|
}
|
|
111
161
|
/**
|
|
112
162
|
* Boot an OTLP/HTTP-based OpenTelemetry pipeline (traces + logs).
|
|
@@ -124,10 +174,13 @@ declare function setupOtel(options: SetupOtelOptions): OtelHandle;
|
|
|
124
174
|
* `setupOtel` has already run in this process.
|
|
125
175
|
*/
|
|
126
176
|
declare function isOtelActive(): boolean;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/version.d.ts
|
|
179
|
+
declare const PHOTON_OTEL_VERSION = "1.1.0";
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/with-span.d.ts
|
|
130
182
|
declare function withSpan<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
|
|
131
183
|
declare function withSpan<T>(name: string, attrs: LogAttrs, fn: () => Promise<T> | T): Promise<T>;
|
|
132
|
-
|
|
133
|
-
export { type FetchInstrumentation, type InstrumentFetchOptions, type LogAttrs, type LogLevel, type OtelHandle, PHOTON_OTEL_VERSION, type PhotonLogger, type SetupOtelOptions, createLogger, getLogLevel, instrumentFetch, isOtelActive, sanitizeEmail, sanitizeErrorMessage, sanitizePhone, setLogLevel, setupOtel, withSpan };
|
|
184
|
+
//#endregion
|
|
185
|
+
export { type FetchInstrumentation, type FetchSpanOptions, type InstrumentFetchOptions, type LogAttrs, type LogLevel, type OtelHandle, PHOTON_OTEL_VERSION, type PhotonLogger, type SetupOtelOptions, createInstrumentedFetch, createLogger, getLogLevel, instrumentFetch, isOtelActive, sanitizeEmail, sanitizeErrorMessage, sanitizePhone, setLogLevel, setupOtel, withSpan };
|
|
186
|
+
//# sourceMappingURL=index.d.ts.map
|