@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/dist/index.js CHANGED
@@ -1,324 +1,574 @@
1
- // src/logger.ts
2
- import { context as otelContext } from "@opentelemetry/api";
3
- import { logs, SeverityNumber } from "@opentelemetry/api-logs";
4
-
5
- // src/version.ts
6
- var PHOTON_OTEL_VERSION = "0.1.0";
7
-
8
- // src/logger.ts
9
- var LEVEL_SEVERITY = {
10
- debug: SeverityNumber.DEBUG,
11
- // 5
12
- info: SeverityNumber.INFO,
13
- // 9
14
- warn: SeverityNumber.WARN,
15
- // 13
16
- error: SeverityNumber.ERROR,
17
- // 17
18
- silent: Number.POSITIVE_INFINITY
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
- var levelOverride;
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
- const raw = process.env.LOG_LEVEL?.toLowerCase();
23
- if (raw && raw in LEVEL_SEVERITY) {
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
- return (process.env.DEPLOYMENT_ENV ?? "development") === "development" ? "debug" : "info";
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
- return envLevel() ?? levelOverride ?? defaultLevel();
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
- levelOverride = level;
263
+ levelOverride = level;
36
264
  }
265
+ /** Current effective log level, after env / override / default resolution. */
37
266
  function getLogLevel() {
38
- return resolveLevel();
267
+ return resolveLevel();
39
268
  }
40
- var scopedLogger;
269
+ let scopedLogger;
41
270
  function getLogger() {
42
- if (!scopedLogger) {
43
- scopedLogger = logs.getLogger("@photon-ai/otel", PHOTON_OTEL_VERSION);
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
- if (!attrs) {
49
- return {};
50
- }
51
- const out = {};
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
- if (severityNumber >= SeverityNumber.ERROR) {
61
- return console.error;
62
- }
63
- if (severityNumber >= SeverityNumber.WARN) {
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
- if (severityNumber < LEVEL_SEVERITY[resolveLevel()]) {
73
- return;
74
- }
75
- const userAttrs = filterUndefined(attrs);
76
- const attributes = {
77
- "log.module": module,
78
- ...userAttrs
79
- };
80
- if (error instanceof Error) {
81
- attributes["exception.type"] = error.name;
82
- attributes["exception.message"] = error.message;
83
- if (error.stack) {
84
- attributes["exception.stacktrace"] = error.stack;
85
- }
86
- } else if (error !== void 0) {
87
- attributes["exception.type"] = typeof error;
88
- attributes["exception.message"] = String(error);
89
- }
90
- getLogger().emit({
91
- severityNumber,
92
- severityText,
93
- body: message,
94
- attributes,
95
- context: otelContext.active()
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
- return {
108
- debug: (message, attrs, error) => emit(SeverityNumber.DEBUG, "DEBUG", module, message, attrs, error),
109
- info: (message, attrs, error) => emit(SeverityNumber.INFO, "INFO", module, message, attrs, error),
110
- warn: (message, attrs, error) => emit(SeverityNumber.WARN, "WARN", module, message, attrs, error),
111
- error: (message, attrs, error) => emit(SeverityNumber.ERROR, "ERROR", module, message, attrs, error)
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
- // src/sanitize.ts
116
- var PHONE_PATTERN = /\+?\d[\d\s()\-.]{6,18}\d/g;
117
- var EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
118
- function sanitizePhone(input) {
119
- const hasPlus = input.startsWith("+");
120
- const digits = input.replace(/\D/g, "");
121
- if (digits.length < 8) {
122
- return hasPlus ? "+xxxx" : "xxxx";
123
- }
124
- const head = digits.slice(0, 3);
125
- const tail = digits.slice(-4);
126
- const middleLength = digits.length - head.length - tail.length;
127
- return `${hasPlus ? "+" : ""}${head}${"x".repeat(middleLength)}${tail}`;
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
- function sanitizeEmail(input) {
130
- const atIndex = input.lastIndexOf("@");
131
- if (atIndex < 1) {
132
- return "***";
133
- }
134
- const local = input.slice(0, atIndex);
135
- const domain = input.slice(atIndex + 1);
136
- const dotIndex = domain.lastIndexOf(".");
137
- if (dotIndex < 1) {
138
- return "***";
139
- }
140
- const localHead = local.slice(0, 2);
141
- const domainHead = domain.slice(0, 1);
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
- function sanitizeErrorMessage(input) {
146
- return input.replace(EMAIL_PATTERN, (match) => sanitizeEmail(match)).replace(PHONE_PATTERN, (match) => sanitizePhone(match));
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
- // src/setup.ts
150
- import { context, trace } from "@opentelemetry/api";
151
- import { logs as logs2 } from "@opentelemetry/api-logs";
152
- import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
153
- import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
154
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
155
- import { resourceFromAttributes } from "@opentelemetry/resources";
156
- import {
157
- BatchLogRecordProcessor,
158
- LoggerProvider
159
- } from "@opentelemetry/sdk-logs";
160
- import {
161
- BasicTracerProvider,
162
- BatchSpanProcessor
163
- } from "@opentelemetry/sdk-trace-base";
164
- var activeHandle;
165
- var TRAILING_SLASH = /\/$/;
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
- if (!raw) {
168
- return {};
169
- }
170
- const out = {};
171
- for (const pair of raw.split(",")) {
172
- const eq = pair.indexOf("=");
173
- if (eq <= 0) {
174
- continue;
175
- }
176
- const key = pair.slice(0, eq).trim();
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
- const traces = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
186
- if (traces) {
187
- return traces;
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
- const logsEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
194
- if (logsEndpoint) {
195
- return logsEndpoint;
196
- }
197
- const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;
198
- return generic ? `${generic.replace(TRAILING_SLASH, "")}/v1/logs` : void 0;
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
- if (activeHandle) {
202
- return activeHandle;
203
- }
204
- if (options.logLevel) {
205
- setLogLevel(options.logLevel);
206
- }
207
- const tracesEndpoint = resolveTracesEndpoint(options.endpoint);
208
- const logsEndpoint = resolveLogsEndpoint(options.endpoint);
209
- const mergedHeaders = {
210
- ...options.headers,
211
- ...parseEnvHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS)
212
- };
213
- const hasHeaders = Object.keys(mergedHeaders).length > 0;
214
- const resource = resourceFromAttributes({
215
- "service.name": options.serviceName,
216
- ...options.serviceVersion ? { "service.version": options.serviceVersion } : {},
217
- "deployment.environment": process.env.DEPLOYMENT_ENV ?? "development",
218
- ...options.resourceAttributes
219
- });
220
- context.setGlobalContextManager(new AsyncLocalStorageContextManager());
221
- const traceProcessors = tracesEndpoint ? [
222
- new BatchSpanProcessor(
223
- new OTLPTraceExporter({
224
- url: tracesEndpoint,
225
- headers: hasHeaders ? mergedHeaders : void 0
226
- })
227
- )
228
- ] : [];
229
- const tracerProvider = new BasicTracerProvider({
230
- resource,
231
- spanProcessors: traceProcessors
232
- });
233
- trace.setGlobalTracerProvider(tracerProvider);
234
- const logProcessors = logsEndpoint ? [
235
- new BatchLogRecordProcessor(
236
- new OTLPLogExporter({
237
- url: logsEndpoint,
238
- headers: hasHeaders ? mergedHeaders : void 0
239
- })
240
- )
241
- ] : [];
242
- const loggerProvider = new LoggerProvider({
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
- return activeHandle !== void 0;
533
+ return activeHandle !== void 0;
261
534
  }
262
-
263
- // src/with-span.ts
264
- import {
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
- if (!scopedTracer) {
271
- scopedTracer = trace2.getTracer("@photon-ai/otel", PHOTON_OTEL_VERSION);
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
- const out = {};
277
- for (const [k, v] of Object.entries(attrs)) {
278
- if (v !== void 0) {
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
- const fn = typeof attrsOrFn === "function" ? attrsOrFn : maybeFn;
286
- if (!fn) {
287
- throw new Error("withSpan: function argument is required");
288
- }
289
- const attrs = typeof attrsOrFn === "function" ? void 0 : attrsOrFn;
290
- return getTracer().startActiveSpan(name, async (span) => {
291
- if (attrs) {
292
- span.setAttributes(toAttributes(attrs));
293
- }
294
- try {
295
- const result = await fn();
296
- span.setStatus({ code: SpanStatusCode.OK });
297
- return result;
298
- } catch (err) {
299
- span.recordException(err);
300
- const errorObj = err instanceof Error ? err : void 0;
301
- span.setAttribute("error.type", errorObj?.constructor.name ?? typeof err);
302
- span.setStatus({
303
- code: SpanStatusCode.ERROR,
304
- message: errorObj ? sanitizeErrorMessage(errorObj.message) : sanitizeErrorMessage(String(err))
305
- });
306
- throw err;
307
- } finally {
308
- span.end();
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