@photon-ai/otel 1.0.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 +86 -2
- package/dist/index.d.ts +128 -37
- package/dist/index.js +534 -284
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -1,324 +1,574 @@
|
|
|
1
|
-
|
|
2
|
-
import { context
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
3
|
+
import { ATTR_ERROR_TYPE, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
|
|
4
|
+
import { SeverityNumber, logs } from "@opentelemetry/api-logs";
|
|
5
|
+
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
|
|
6
|
+
import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
7
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
8
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
9
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
10
|
+
import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
11
|
+
import { BasicTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
12
|
+
//#region src/sanitize.ts
|
|
13
|
+
const PHONE_PATTERN = /\+?\d[\d\s()\-.]{6,18}\d/g;
|
|
14
|
+
const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
15
|
+
/**
|
|
16
|
+
* Mask a phone number, keeping the leading `+` (if any) plus the first 3 digits
|
|
17
|
+
* and the last 4 digits visible. Example: `+13315553374` -> `+133xxxxx3374`.
|
|
18
|
+
*
|
|
19
|
+
* Inputs that don't have enough digits to safely mask are returned as
|
|
20
|
+
* `xxxx` to avoid leaking the entire short value.
|
|
21
|
+
*/
|
|
22
|
+
function sanitizePhone(input) {
|
|
23
|
+
const hasPlus = input.startsWith("+");
|
|
24
|
+
const digits = input.replace(/\D/g, "");
|
|
25
|
+
if (digits.length < 8) return hasPlus ? "+xxxx" : "xxxx";
|
|
26
|
+
const head = digits.slice(0, 3);
|
|
27
|
+
const tail = digits.slice(-4);
|
|
28
|
+
const middleLength = digits.length - head.length - tail.length;
|
|
29
|
+
return `${hasPlus ? "+" : ""}${head}${"x".repeat(middleLength)}${tail}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Mask an email address, keeping the first 2 chars of the local part, the
|
|
33
|
+
* first char of the domain, and the TLD. Example:
|
|
34
|
+
* `foo.bar@example.com` -> `fo***@e***.com`.
|
|
35
|
+
*/
|
|
36
|
+
function sanitizeEmail(input) {
|
|
37
|
+
const atIndex = input.lastIndexOf("@");
|
|
38
|
+
if (atIndex < 1) return "***";
|
|
39
|
+
const local = input.slice(0, atIndex);
|
|
40
|
+
const domain = input.slice(atIndex + 1);
|
|
41
|
+
const dotIndex = domain.lastIndexOf(".");
|
|
42
|
+
if (dotIndex < 1) return "***";
|
|
43
|
+
return `${local.slice(0, 2)}***@${domain.slice(0, 1)}***${domain.slice(dotIndex)}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Replace every phone number and email address inside a free-form string with
|
|
47
|
+
* its sanitized form. Used to scrub `Error.message` values before attaching
|
|
48
|
+
* them to span status.
|
|
49
|
+
*/
|
|
50
|
+
function sanitizeErrorMessage(input) {
|
|
51
|
+
return input.replace(EMAIL_PATTERN, (match) => sanitizeEmail(match)).replace(PHONE_PATTERN, (match) => sanitizePhone(match));
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/version.ts
|
|
55
|
+
const PHOTON_OTEL_VERSION = "1.1.0";
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/instrument-fetch.ts
|
|
58
|
+
/**
|
|
59
|
+
* Stored on the wrapper via the global symbol registry (`Symbol.for`) so the
|
|
60
|
+
* double-wrap guard holds even when two copies of this module load — which can
|
|
61
|
+
* happen because the `bun` export condition serves `src/` while `default`
|
|
62
|
+
* serves `dist/`.
|
|
63
|
+
*/
|
|
64
|
+
const PATCH_MARKER = Symbol.for("@photon-ai/otel.fetch.original");
|
|
65
|
+
const HTTP_ERROR_STATUS_MIN = 400;
|
|
66
|
+
const DEFAULT_PORTS = {
|
|
67
|
+
"https:": 443,
|
|
68
|
+
"http:": 80
|
|
19
69
|
};
|
|
20
|
-
|
|
70
|
+
let scopedTracer$1;
|
|
71
|
+
function getTracer$1() {
|
|
72
|
+
if (!scopedTracer$1) scopedTracer$1 = trace.getTracer("@photon-ai/otel", PHOTON_OTEL_VERSION);
|
|
73
|
+
return scopedTracer$1;
|
|
74
|
+
}
|
|
75
|
+
function setGlobalFetch(fn) {
|
|
76
|
+
globalThis.fetch = fn;
|
|
77
|
+
}
|
|
78
|
+
function getPatchOriginal(fn) {
|
|
79
|
+
return fn[PATCH_MARKER];
|
|
80
|
+
}
|
|
81
|
+
function setPatchOriginal(fn, original) {
|
|
82
|
+
fn[PATCH_MARKER] = original;
|
|
83
|
+
}
|
|
84
|
+
/** Copy extra own properties (e.g. Bun's `fetch.preconnect`) onto the wrapper. */
|
|
85
|
+
function preserveProps(from, to) {
|
|
86
|
+
for (const key of Object.getOwnPropertyNames(from)) {
|
|
87
|
+
if (key in to) continue;
|
|
88
|
+
const descriptor = Object.getOwnPropertyDescriptor(from, key);
|
|
89
|
+
if (descriptor) Object.defineProperty(to, key, descriptor);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function resolveRequestMeta(input, init) {
|
|
93
|
+
if (input instanceof Request) return {
|
|
94
|
+
method: input.method,
|
|
95
|
+
url: input.url
|
|
96
|
+
};
|
|
97
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
98
|
+
return {
|
|
99
|
+
method: init?.method ?? "GET",
|
|
100
|
+
url
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function resolvePort(parsed) {
|
|
104
|
+
if (parsed.port) return Number(parsed.port);
|
|
105
|
+
return DEFAULT_PORTS[parsed.protocol];
|
|
106
|
+
}
|
|
107
|
+
function toAttributes$1(attrs) {
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const [key, value] of Object.entries(attrs)) if (value !== void 0) out[key] = value;
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
function fetchAttributes(method, url) {
|
|
113
|
+
const attrs = {
|
|
114
|
+
[ATTR_HTTP_REQUEST_METHOD]: method,
|
|
115
|
+
[ATTR_URL_FULL]: url
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
const parsed = new URL(url);
|
|
119
|
+
attrs[ATTR_SERVER_ADDRESS] = parsed.hostname || void 0;
|
|
120
|
+
attrs[ATTR_SERVER_PORT] = resolvePort(parsed);
|
|
121
|
+
} catch {}
|
|
122
|
+
return toAttributes$1(attrs);
|
|
123
|
+
}
|
|
124
|
+
/** Build the outgoing headers and inject the active trace context into them. */
|
|
125
|
+
function buildPropagatedHeaders(input, init) {
|
|
126
|
+
const headers = new Headers(input instanceof Request ? input.headers : void 0);
|
|
127
|
+
if (init?.headers) for (const [key, value] of new Headers(init.headers).entries()) headers.set(key, value);
|
|
128
|
+
propagation.inject(context.active(), headers, { set: (carrier, key, value) => {
|
|
129
|
+
carrier.set(key, value);
|
|
130
|
+
} });
|
|
131
|
+
return headers;
|
|
132
|
+
}
|
|
133
|
+
function callOriginal(original, input, init, headers) {
|
|
134
|
+
if (input instanceof Request) {
|
|
135
|
+
if (input.bodyUsed) {
|
|
136
|
+
for (const [key, value] of headers.entries()) input.headers.set(key, value);
|
|
137
|
+
return original(input, init);
|
|
138
|
+
}
|
|
139
|
+
return original(new Request(input, {
|
|
140
|
+
...init,
|
|
141
|
+
headers
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
return original(input, {
|
|
145
|
+
...init,
|
|
146
|
+
headers
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function buildWrappedFetch(original, options) {
|
|
150
|
+
const staticAttributes = options?.attributes;
|
|
151
|
+
return (input, init) => {
|
|
152
|
+
const { method, url } = resolveRequestMeta(input, init);
|
|
153
|
+
if (options?.ignore?.(url)) return original(input, init);
|
|
154
|
+
const name = method.toUpperCase();
|
|
155
|
+
return getTracer$1().startActiveSpan(name, { kind: SpanKind.CLIENT }, async (span) => {
|
|
156
|
+
if (staticAttributes) span.setAttributes(staticAttributes);
|
|
157
|
+
span.setAttributes(fetchAttributes(name, url));
|
|
158
|
+
try {
|
|
159
|
+
const response = await callOriginal(original, input, init, buildPropagatedHeaders(input, init));
|
|
160
|
+
span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
|
161
|
+
span.setStatus({ code: response.status >= HTTP_ERROR_STATUS_MIN ? SpanStatusCode.ERROR : SpanStatusCode.OK });
|
|
162
|
+
return response;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
span.recordException(err);
|
|
165
|
+
const errorObj = err instanceof Error ? err : void 0;
|
|
166
|
+
span.setAttribute(ATTR_ERROR_TYPE, errorObj?.constructor.name ?? typeof err);
|
|
167
|
+
span.setStatus({
|
|
168
|
+
code: SpanStatusCode.ERROR,
|
|
169
|
+
message: errorObj ? sanitizeErrorMessage(errorObj.message) : sanitizeErrorMessage(String(err))
|
|
170
|
+
});
|
|
171
|
+
throw err;
|
|
172
|
+
} finally {
|
|
173
|
+
span.end();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Wrap a single fetch function — not `globalThis.fetch` — so requests made
|
|
180
|
+
* through the RETURNED fetch produce a CLIENT span and carry W3C trace context
|
|
181
|
+
* to the downstream service.
|
|
182
|
+
*
|
|
183
|
+
* Built for SDKs that accept a `fetch` option, e.g.
|
|
184
|
+
* `new OpenAI({ fetch: createInstrumentedFetch() })`. Unlike `instrumentFetch`,
|
|
185
|
+
* it never mutates the global and has no lifecycle to unpatch — it just returns
|
|
186
|
+
* a new fetch you pass where you need it.
|
|
187
|
+
*
|
|
188
|
+
* `baseFetch` defaults to the current `globalThis.fetch`, read at call time.
|
|
189
|
+
* Idempotent: passing an already-instrumented fetch returns it unchanged.
|
|
190
|
+
*
|
|
191
|
+
* Always uses the global-wrap technique (the native undici instrumentation
|
|
192
|
+
* cannot target a single instance), so it behaves identically on Bun and Node.
|
|
193
|
+
* On Node, if `setupOtel`'s global fetch instrumentation is also active, the
|
|
194
|
+
* SDK's request is captured twice — disable it (`instrumentFetch: false`) for
|
|
195
|
+
* paths you instrument per-instance.
|
|
196
|
+
*/
|
|
197
|
+
function createInstrumentedFetch(baseFetch = globalThis.fetch, options) {
|
|
198
|
+
if (getPatchOriginal(baseFetch)) return baseFetch;
|
|
199
|
+
const wrapped = buildWrappedFetch(baseFetch, options);
|
|
200
|
+
preserveProps(baseFetch, wrapped);
|
|
201
|
+
setPatchOriginal(wrapped, baseFetch);
|
|
202
|
+
return wrapped;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Wrap `globalThis.fetch` so every outbound request produces a CLIENT span and
|
|
206
|
+
* carries W3C trace context to the downstream service.
|
|
207
|
+
*
|
|
208
|
+
* On Bun this is the only fetch instrumentation that works: Bun's native fetch
|
|
209
|
+
* emits no `diagnostics_channel` events, so the standard `instrumentation-undici`
|
|
210
|
+
* / `instrumentation-http` (and `opentelemetry-instrumentation-fetch-node`,
|
|
211
|
+
* which is itself diagnostics_channel-based) produce no spans. It works
|
|
212
|
+
* identically on Node, where `globalThis.fetch` is undici-backed.
|
|
213
|
+
*
|
|
214
|
+
* Idempotent: a second call does not stack another wrapper. Returns a handle
|
|
215
|
+
* whose `unpatch()` restores the original fetch.
|
|
216
|
+
*/
|
|
217
|
+
function instrumentFetch(options) {
|
|
218
|
+
const current = globalThis.fetch;
|
|
219
|
+
const existingOriginal = getPatchOriginal(current);
|
|
220
|
+
if (existingOriginal) return { unpatch() {
|
|
221
|
+
if (globalThis.fetch === current) setGlobalFetch(existingOriginal);
|
|
222
|
+
} };
|
|
223
|
+
const original = current;
|
|
224
|
+
const wrapped = createInstrumentedFetch(original, options);
|
|
225
|
+
setGlobalFetch(wrapped);
|
|
226
|
+
return { unpatch() {
|
|
227
|
+
if (globalThis.fetch === wrapped) setGlobalFetch(original);
|
|
228
|
+
} };
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
231
|
+
//#region src/logger.ts
|
|
232
|
+
const LEVEL_SEVERITY = {
|
|
233
|
+
debug: SeverityNumber.DEBUG,
|
|
234
|
+
info: SeverityNumber.INFO,
|
|
235
|
+
warn: SeverityNumber.WARN,
|
|
236
|
+
error: SeverityNumber.ERROR,
|
|
237
|
+
silent: Number.POSITIVE_INFINITY
|
|
238
|
+
};
|
|
239
|
+
let levelOverride;
|
|
21
240
|
function envLevel() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return raw;
|
|
25
|
-
}
|
|
26
|
-
return;
|
|
241
|
+
const raw = process.env.LOG_LEVEL?.toLowerCase();
|
|
242
|
+
if (raw && raw in LEVEL_SEVERITY) return raw;
|
|
27
243
|
}
|
|
28
244
|
function defaultLevel() {
|
|
29
|
-
|
|
245
|
+
return (process.env.DEPLOYMENT_ENV ?? "development") === "development" ? "debug" : "info";
|
|
30
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Resolve the active level fresh on each call so that `LOG_LEVEL` changes and
|
|
249
|
+
* `setLogLevel()` both take effect immediately. Resolution order (env wins, to
|
|
250
|
+
* match the rest of the package's config story):
|
|
251
|
+
* 1. `LOG_LEVEL` env var
|
|
252
|
+
* 2. `setLogLevel()` / `setupOtel({ logLevel })`
|
|
253
|
+
* 3. environment-driven default (`debug` in development, `info` otherwise)
|
|
254
|
+
*/
|
|
31
255
|
function resolveLevel() {
|
|
32
|
-
|
|
256
|
+
return envLevel() ?? levelOverride ?? defaultLevel();
|
|
33
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Programmatically set the minimum log level. Takes effect immediately for
|
|
260
|
+
* subsequent logs. `LOG_LEVEL` env var still wins if set.
|
|
261
|
+
*/
|
|
34
262
|
function setLogLevel(level) {
|
|
35
|
-
|
|
263
|
+
levelOverride = level;
|
|
36
264
|
}
|
|
265
|
+
/** Current effective log level, after env / override / default resolution. */
|
|
37
266
|
function getLogLevel() {
|
|
38
|
-
|
|
267
|
+
return resolveLevel();
|
|
39
268
|
}
|
|
40
|
-
|
|
269
|
+
let scopedLogger;
|
|
41
270
|
function getLogger() {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
return scopedLogger;
|
|
271
|
+
if (!scopedLogger) scopedLogger = logs.getLogger("@photon-ai/otel", PHOTON_OTEL_VERSION);
|
|
272
|
+
return scopedLogger;
|
|
46
273
|
}
|
|
47
274
|
function filterUndefined(attrs) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (const [k, v] of Object.entries(attrs)) {
|
|
53
|
-
if (v !== void 0) {
|
|
54
|
-
out[k] = v;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return out;
|
|
275
|
+
if (!attrs) return {};
|
|
276
|
+
const out = {};
|
|
277
|
+
for (const [k, v] of Object.entries(attrs)) if (v !== void 0) out[k] = v;
|
|
278
|
+
return out;
|
|
58
279
|
}
|
|
59
280
|
function consoleFor(severityNumber) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return console.warn;
|
|
65
|
-
}
|
|
66
|
-
if (severityNumber >= SeverityNumber.INFO) {
|
|
67
|
-
return console.info;
|
|
68
|
-
}
|
|
69
|
-
return console.debug;
|
|
281
|
+
if (severityNumber >= SeverityNumber.ERROR) return console.error;
|
|
282
|
+
if (severityNumber >= SeverityNumber.WARN) return console.warn;
|
|
283
|
+
if (severityNumber >= SeverityNumber.INFO) return console.info;
|
|
284
|
+
return console.debug;
|
|
70
285
|
}
|
|
71
286
|
function emit(severityNumber, severityText, module, message, attrs, error) {
|
|
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
|
-
const extras = [];
|
|
98
|
-
if (Object.keys(userAttrs).length > 0) {
|
|
99
|
-
extras.push(userAttrs);
|
|
100
|
-
}
|
|
101
|
-
if (error !== void 0) {
|
|
102
|
-
extras.push(error);
|
|
103
|
-
}
|
|
104
|
-
consoleFor(severityNumber)(`[${module}]`, severityText, message, ...extras);
|
|
287
|
+
if (severityNumber < LEVEL_SEVERITY[resolveLevel()]) return;
|
|
288
|
+
const userAttrs = filterUndefined(attrs);
|
|
289
|
+
const attributes = {
|
|
290
|
+
"log.module": module,
|
|
291
|
+
...userAttrs
|
|
292
|
+
};
|
|
293
|
+
if (error instanceof Error) {
|
|
294
|
+
attributes["exception.type"] = error.name;
|
|
295
|
+
attributes["exception.message"] = error.message;
|
|
296
|
+
if (error.stack) attributes["exception.stacktrace"] = error.stack;
|
|
297
|
+
} else if (error !== void 0) {
|
|
298
|
+
attributes["exception.type"] = typeof error;
|
|
299
|
+
attributes["exception.message"] = String(error);
|
|
300
|
+
}
|
|
301
|
+
getLogger().emit({
|
|
302
|
+
severityNumber,
|
|
303
|
+
severityText,
|
|
304
|
+
body: message,
|
|
305
|
+
attributes,
|
|
306
|
+
context: context.active()
|
|
307
|
+
});
|
|
308
|
+
const extras = [];
|
|
309
|
+
if (Object.keys(userAttrs).length > 0) extras.push(userAttrs);
|
|
310
|
+
if (error !== void 0) extras.push(error);
|
|
311
|
+
consoleFor(severityNumber)(`[${module}]`, severityText, message, ...extras);
|
|
105
312
|
}
|
|
106
313
|
function createLogger(module) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
314
|
+
return {
|
|
315
|
+
debug: (message, attrs, error) => emit(SeverityNumber.DEBUG, "DEBUG", module, message, attrs, error),
|
|
316
|
+
info: (message, attrs, error) => emit(SeverityNumber.INFO, "INFO", module, message, attrs, error),
|
|
317
|
+
warn: (message, attrs, error) => emit(SeverityNumber.WARN, "WARN", module, message, attrs, error),
|
|
318
|
+
error: (message, attrs, error) => emit(SeverityNumber.ERROR, "ERROR", module, message, attrs, error)
|
|
319
|
+
};
|
|
113
320
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/instrument-fetch-native.ts
|
|
323
|
+
/**
|
|
324
|
+
* Reconstruct the absolute URL undici describes from `origin` + `path`, matching
|
|
325
|
+
* how the instrumentation builds `url.full`. This lets the caller's `ignore(url)`
|
|
326
|
+
* predicate (and the OTLP self-trace exclusion) behave identically to the
|
|
327
|
+
* global-wrap path.
|
|
328
|
+
*/
|
|
329
|
+
function toAbsoluteUrl(request) {
|
|
330
|
+
try {
|
|
331
|
+
return new URL(request.path, request.origin).toString();
|
|
332
|
+
} catch {
|
|
333
|
+
return `${request.origin}${request.path}`;
|
|
334
|
+
}
|
|
128
335
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const tld = domain.slice(dotIndex);
|
|
143
|
-
return `${localHead}***@${domainHead}***${tld}`;
|
|
336
|
+
/** Node's "module isn't installed" errors carry one of these messages. */
|
|
337
|
+
const MODULE_NOT_FOUND_MESSAGE = /Cannot find (module|package)/;
|
|
338
|
+
/**
|
|
339
|
+
* True only when `error` signals an optional package being absent, so the caller
|
|
340
|
+
* can safely fall back to the `globalThis.fetch` wrap. A version mismatch or a
|
|
341
|
+
* throw from the package's own initialization is a real failure and must be
|
|
342
|
+
* rethrown rather than masked as "not installed".
|
|
343
|
+
*/
|
|
344
|
+
function isModuleNotFoundError(error) {
|
|
345
|
+
if (typeof error !== "object" || error === null) return false;
|
|
346
|
+
const { code, message } = error;
|
|
347
|
+
if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") return true;
|
|
348
|
+
return typeof message === "string" && MODULE_NOT_FOUND_MESSAGE.test(message);
|
|
144
349
|
}
|
|
145
|
-
|
|
146
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Register `@opentelemetry/instrumentation-undici` — Node's native fetch
|
|
352
|
+
* instrumentation, which reads the global tracer provider and propagator that
|
|
353
|
+
* `setupOtel()` installs. Returns `undefined` when the optional packages aren't
|
|
354
|
+
* installed, or when static `attributes` are requested (the undici path has no
|
|
355
|
+
* hook to stamp them on every span), so the caller can fall back to the
|
|
356
|
+
* `globalThis.fetch` wrap.
|
|
357
|
+
*
|
|
358
|
+
* The packages are referenced only through `requireFn(...)` string calls (never
|
|
359
|
+
* a static `import`), so esbuild can't bundle them and Bun never loads them.
|
|
360
|
+
*/
|
|
361
|
+
function instrumentFetchNative(options, requireFn) {
|
|
362
|
+
if (options?.attributes && Object.keys(options.attributes).length > 0) return;
|
|
363
|
+
let UndiciInstrumentation;
|
|
364
|
+
let registerInstrumentations;
|
|
365
|
+
try {
|
|
366
|
+
const undiciModule = requireFn("@opentelemetry/instrumentation-undici");
|
|
367
|
+
const instrumentationModule = requireFn("@opentelemetry/instrumentation");
|
|
368
|
+
UndiciInstrumentation = undiciModule.UndiciInstrumentation;
|
|
369
|
+
registerInstrumentations = instrumentationModule.registerInstrumentations;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (isModuleNotFoundError(error)) return;
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
const userIgnore = options?.ignore;
|
|
375
|
+
const instrumentation = new UndiciInstrumentation({ ignoreRequestHook: userIgnore ? (request) => userIgnore(toAbsoluteUrl(request)) : void 0 });
|
|
376
|
+
registerInstrumentations({ instrumentations: [instrumentation] });
|
|
377
|
+
return { unpatch() {
|
|
378
|
+
instrumentation.disable();
|
|
379
|
+
} };
|
|
147
380
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/runtime.ts
|
|
383
|
+
/**
|
|
384
|
+
* `true` when running on Bun, `false` on Node (or any other runtime).
|
|
385
|
+
*
|
|
386
|
+
* This is the one place the library detects its runtime. Bun's native `fetch`
|
|
387
|
+
* emits no `diagnostics_channel` events, so the official OpenTelemetry
|
|
388
|
+
* instrumentations (`instrumentation-undici` / `-http`) produce no spans there
|
|
389
|
+
* — we must wrap `globalThis.fetch` instead. On Node we prefer the native
|
|
390
|
+
* undici instrumentation. `setupOtel()` branches on this constant.
|
|
391
|
+
*
|
|
392
|
+
* `process.versions.bun` is the canonical signal: it survives deletion of the
|
|
393
|
+
* `Bun` global and matches the codebase's `process.*` convention. Evaluated
|
|
394
|
+
* once at module load — the runtime never changes mid-process.
|
|
395
|
+
*/
|
|
396
|
+
const IS_BUN = typeof process !== "undefined" && process.versions?.bun !== void 0;
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/setup.ts
|
|
399
|
+
let activeHandle;
|
|
400
|
+
const TRAILING_SLASH = /\/$/;
|
|
166
401
|
function parseEnvHeaders(raw) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const value = pair.slice(eq + 1).trim();
|
|
178
|
-
if (key) {
|
|
179
|
-
out[key] = value;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return out;
|
|
402
|
+
if (!raw) return {};
|
|
403
|
+
const out = {};
|
|
404
|
+
for (const pair of raw.split(",")) {
|
|
405
|
+
const eq = pair.indexOf("=");
|
|
406
|
+
if (eq <= 0) continue;
|
|
407
|
+
const key = pair.slice(0, eq).trim();
|
|
408
|
+
const value = pair.slice(eq + 1).trim();
|
|
409
|
+
if (key) out[key] = value;
|
|
410
|
+
}
|
|
411
|
+
return out;
|
|
183
412
|
}
|
|
184
413
|
function resolveTracesEndpoint(base) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;
|
|
190
|
-
return generic ? `${generic.replace(TRAILING_SLASH, "")}/v1/traces` : void 0;
|
|
414
|
+
const traces = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
|
|
415
|
+
if (traces) return traces;
|
|
416
|
+
const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;
|
|
417
|
+
return generic ? `${generic.replace(TRAILING_SLASH, "")}/v1/traces` : void 0;
|
|
191
418
|
}
|
|
192
419
|
function resolveLogsEndpoint(base) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
420
|
+
const logsEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
|
|
421
|
+
if (logsEndpoint) return logsEndpoint;
|
|
422
|
+
const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;
|
|
423
|
+
return generic ? `${generic.replace(TRAILING_SLASH, "")}/v1/logs` : void 0;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Normalize a URL to an `origin + path` key (trailing slash stripped) for exact
|
|
427
|
+
* self-trace matching. Returns `undefined` for unparseable URLs.
|
|
428
|
+
*/
|
|
429
|
+
function otlpEndpointKey(url) {
|
|
430
|
+
try {
|
|
431
|
+
const parsed = new URL(url);
|
|
432
|
+
return `${parsed.origin}${parsed.pathname.replace(TRAILING_SLASH, "")}`;
|
|
433
|
+
} catch {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function otlpEndpointKeysOf(tracesEndpoint, logsEndpoint) {
|
|
438
|
+
const keys = [];
|
|
439
|
+
for (const endpoint of [tracesEndpoint, logsEndpoint]) {
|
|
440
|
+
if (!endpoint) continue;
|
|
441
|
+
const key = otlpEndpointKey(endpoint);
|
|
442
|
+
if (key) keys.push(key);
|
|
443
|
+
}
|
|
444
|
+
return keys;
|
|
199
445
|
}
|
|
446
|
+
/**
|
|
447
|
+
* Start fetch instrumentation unless disabled. Defaults to on when a traces
|
|
448
|
+
* pipeline is configured. On Node (mode `"auto"`) this registers the native
|
|
449
|
+
* `@opentelemetry/instrumentation-undici`; on Bun, or with mode `"global"`, it
|
|
450
|
+
* wraps `globalThis.fetch`. Always excludes our own OTLP endpoints so the
|
|
451
|
+
* exporter's traffic is never self-traced (matters on Node, where the OTLP
|
|
452
|
+
* exporter can use fetch).
|
|
453
|
+
*/
|
|
454
|
+
function startFetchInstrumentation(option, hasTraces, tracesEndpoint, logsEndpoint) {
|
|
455
|
+
if (!(option ?? hasTraces)) return;
|
|
456
|
+
const userOptions = typeof option === "object" ? option : void 0;
|
|
457
|
+
const otlpEndpointKeys = otlpEndpointKeysOf(tracesEndpoint, logsEndpoint);
|
|
458
|
+
const ignore = (url) => {
|
|
459
|
+
const key = otlpEndpointKey(url);
|
|
460
|
+
return key !== void 0 && otlpEndpointKeys.includes(key) || (userOptions?.ignore?.(url) ?? false);
|
|
461
|
+
};
|
|
462
|
+
if ((userOptions?.mode ?? "auto") === "auto" && !IS_BUN) {
|
|
463
|
+
const native = instrumentFetchNative({
|
|
464
|
+
...userOptions,
|
|
465
|
+
ignore
|
|
466
|
+
}, createRequire(import.meta.url));
|
|
467
|
+
if (native) return native;
|
|
468
|
+
}
|
|
469
|
+
return instrumentFetch({
|
|
470
|
+
...userOptions,
|
|
471
|
+
ignore
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Boot an OTLP/HTTP-based OpenTelemetry pipeline (traces + logs).
|
|
476
|
+
*
|
|
477
|
+
* Idempotent: calling twice in the same process is a no-op on the second
|
|
478
|
+
* call, so libraries can safely invoke this without clobbering an app-level
|
|
479
|
+
* OTel setup that ran earlier.
|
|
480
|
+
*
|
|
481
|
+
* Standard `OTEL_EXPORTER_OTLP_*` env vars override the `endpoint` and
|
|
482
|
+
* `headers` arguments — this matches the OpenTelemetry SDK config spec.
|
|
483
|
+
*/
|
|
200
484
|
function setupOtel(options) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
resource,
|
|
244
|
-
processors: logProcessors
|
|
245
|
-
});
|
|
246
|
-
logs2.setGlobalLoggerProvider(loggerProvider);
|
|
247
|
-
const handle = {
|
|
248
|
-
async shutdown() {
|
|
249
|
-
await Promise.allSettled([
|
|
250
|
-
tracerProvider.shutdown(),
|
|
251
|
-
loggerProvider.shutdown()
|
|
252
|
-
]);
|
|
253
|
-
activeHandle = void 0;
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
activeHandle = handle;
|
|
257
|
-
return handle;
|
|
485
|
+
if (activeHandle) return activeHandle;
|
|
486
|
+
if (options.logLevel) setLogLevel(options.logLevel);
|
|
487
|
+
const tracesEndpoint = resolveTracesEndpoint(options.endpoint);
|
|
488
|
+
const logsEndpoint = resolveLogsEndpoint(options.endpoint);
|
|
489
|
+
const mergedHeaders = {
|
|
490
|
+
...options.headers,
|
|
491
|
+
...parseEnvHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS)
|
|
492
|
+
};
|
|
493
|
+
const hasHeaders = Object.keys(mergedHeaders).length > 0;
|
|
494
|
+
const resource = resourceFromAttributes({
|
|
495
|
+
"service.name": options.serviceName,
|
|
496
|
+
...options.serviceVersion ? { "service.version": options.serviceVersion } : {},
|
|
497
|
+
"deployment.environment": process.env.DEPLOYMENT_ENV ?? "development",
|
|
498
|
+
...options.resourceAttributes
|
|
499
|
+
});
|
|
500
|
+
context.setGlobalContextManager(new AsyncLocalStorageContextManager());
|
|
501
|
+
propagation.setGlobalPropagator(new CompositePropagator({ propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()] }));
|
|
502
|
+
const traceProcessors = tracesEndpoint ? [new BatchSpanProcessor(new OTLPTraceExporter({
|
|
503
|
+
url: tracesEndpoint,
|
|
504
|
+
headers: hasHeaders ? mergedHeaders : void 0
|
|
505
|
+
}))] : [];
|
|
506
|
+
const tracerProvider = new BasicTracerProvider({
|
|
507
|
+
resource,
|
|
508
|
+
spanProcessors: traceProcessors
|
|
509
|
+
});
|
|
510
|
+
trace.setGlobalTracerProvider(tracerProvider);
|
|
511
|
+
const fetchInstrumentation = startFetchInstrumentation(options.instrumentFetch, traceProcessors.length > 0, tracesEndpoint, logsEndpoint);
|
|
512
|
+
const loggerProvider = new LoggerProvider({
|
|
513
|
+
resource,
|
|
514
|
+
processors: logsEndpoint ? [new BatchLogRecordProcessor(new OTLPLogExporter({
|
|
515
|
+
url: logsEndpoint,
|
|
516
|
+
headers: hasHeaders ? mergedHeaders : void 0
|
|
517
|
+
}))] : []
|
|
518
|
+
});
|
|
519
|
+
logs.setGlobalLoggerProvider(loggerProvider);
|
|
520
|
+
const handle = { async shutdown() {
|
|
521
|
+
fetchInstrumentation?.unpatch();
|
|
522
|
+
await Promise.allSettled([tracerProvider.shutdown(), loggerProvider.shutdown()]);
|
|
523
|
+
activeHandle = void 0;
|
|
524
|
+
} };
|
|
525
|
+
activeHandle = handle;
|
|
526
|
+
return handle;
|
|
258
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Read-only accessor for tests / debug paths that need to know whether
|
|
530
|
+
* `setupOtel` has already run in this process.
|
|
531
|
+
*/
|
|
259
532
|
function isOtelActive() {
|
|
260
|
-
|
|
533
|
+
return activeHandle !== void 0;
|
|
261
534
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
SpanStatusCode,
|
|
266
|
-
trace as trace2
|
|
267
|
-
} from "@opentelemetry/api";
|
|
268
|
-
var scopedTracer;
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/with-span.ts
|
|
537
|
+
let scopedTracer;
|
|
269
538
|
function getTracer() {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
return scopedTracer;
|
|
539
|
+
if (!scopedTracer) scopedTracer = trace.getTracer("@photon-ai/otel", PHOTON_OTEL_VERSION);
|
|
540
|
+
return scopedTracer;
|
|
274
541
|
}
|
|
275
542
|
function toAttributes(attrs) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
out[k] = v;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return out;
|
|
543
|
+
const out = {};
|
|
544
|
+
for (const [k, v] of Object.entries(attrs)) if (v !== void 0) out[k] = v;
|
|
545
|
+
return out;
|
|
283
546
|
}
|
|
284
547
|
function withSpan(name, attrsOrFn, maybeFn) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
export {
|
|
313
|
-
PHOTON_OTEL_VERSION,
|
|
314
|
-
createLogger,
|
|
315
|
-
getLogLevel,
|
|
316
|
-
isOtelActive,
|
|
317
|
-
sanitizeEmail,
|
|
318
|
-
sanitizeErrorMessage,
|
|
319
|
-
sanitizePhone,
|
|
320
|
-
setLogLevel,
|
|
321
|
-
setupOtel,
|
|
322
|
-
withSpan
|
|
323
|
-
};
|
|
548
|
+
const fn = typeof attrsOrFn === "function" ? attrsOrFn : maybeFn;
|
|
549
|
+
if (!fn) throw new Error("withSpan: function argument is required");
|
|
550
|
+
const attrs = typeof attrsOrFn === "function" ? void 0 : attrsOrFn;
|
|
551
|
+
return getTracer().startActiveSpan(name, async (span) => {
|
|
552
|
+
if (attrs) span.setAttributes(toAttributes(attrs));
|
|
553
|
+
try {
|
|
554
|
+
const result = await fn();
|
|
555
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
556
|
+
return result;
|
|
557
|
+
} catch (err) {
|
|
558
|
+
span.recordException(err);
|
|
559
|
+
const errorObj = err instanceof Error ? err : void 0;
|
|
560
|
+
span.setAttribute("error.type", errorObj?.constructor.name ?? typeof err);
|
|
561
|
+
span.setStatus({
|
|
562
|
+
code: SpanStatusCode.ERROR,
|
|
563
|
+
message: errorObj ? sanitizeErrorMessage(errorObj.message) : sanitizeErrorMessage(String(err))
|
|
564
|
+
});
|
|
565
|
+
throw err;
|
|
566
|
+
} finally {
|
|
567
|
+
span.end();
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
//#endregion
|
|
572
|
+
export { PHOTON_OTEL_VERSION, createInstrumentedFetch, createLogger, getLogLevel, instrumentFetch, isOtelActive, sanitizeEmail, sanitizeErrorMessage, sanitizePhone, setLogLevel, setupOtel, withSpan };
|
|
573
|
+
|
|
324
574
|
//# sourceMappingURL=index.js.map
|