@photon-ai/otel 1.0.0 → 1.1.0

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 CHANGED
@@ -7,6 +7,7 @@ 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()` wraps `globalThis.fetch` so every outbound request gets a CLIENT span and W3C trace-context headers. On **Bun** this is the only fetch instrumentation that works — the standard `diagnostics_channel`-based OTel instrumentations emit nothing for Bun's native fetch — and it behaves identically on Node.
10
11
  - **`sanitizeEmail` / `sanitizePhone` / `sanitizeErrorMessage`** — PII helpers you can reuse anywhere.
11
12
 
12
13
  OTLP/HTTP only (no gRPC, no proto). Works identically on Bun and Node ≥ 20.
@@ -34,7 +35,9 @@ const log = createLogger("server");
34
35
 
35
36
  await withSpan("handle-request", { route: "/users" }, async () => {
36
37
  log.info("processing request", { userId: 42 });
37
- // ... your work
38
+ // Outbound fetch is traced automatically: a CLIENT span, parented to this
39
+ // one, with a `traceparent` header injected for the downstream service.
40
+ await fetch("https://api.example.com/users");
38
41
  });
39
42
  ```
40
43
 
@@ -46,6 +49,7 @@ If `OTEL_EXPORTER_OTLP_ENDPOINT` (or the `endpoint` option) is unset, `setupOtel
46
49
  | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
47
50
  | `setupOtel(options): OtelHandle` | Boots OTLP/HTTP traces + logs. Idempotent. Returns `{ shutdown(): Promise<void> }`. |
48
51
  | `isOtelActive(): boolean` | Returns `true` if `setupOtel` has already run in this process. |
52
+ | `instrumentFetch(options?): FetchInstrumentation` | Wraps `globalThis.fetch` for CLIENT spans + W3C propagation. Auto-enabled by `setupOtel` when traces are configured. Returns `{ unpatch() }`. |
49
53
  | `createLogger(module): PhotonLogger` | Returns `{ info, warn, error, debug }`. Each call emits to OTel + `console`, correlates to active span. |
50
54
  | `setLogLevel(level): void` | Set the minimum level emitted (`debug`/`info`/`warn`/`error`/`silent`). `LOG_LEVEL` env still wins. |
51
55
  | `getLogLevel(): LogLevel` | Current effective level after env / override / default resolution. |
@@ -109,6 +113,30 @@ Standard OpenTelemetry env vars always take precedence over `SetupOtelOptions`:
109
113
  | `DEPLOYMENT_ENV` | Attached as `deployment.environment` resource attribute. Defaults to `development`. Also drives the default log level. |
110
114
  | `LOG_LEVEL` | Minimum log level: `debug` \| `info` \| `warn` \| `error` \| `silent`. Overrides `setLogLevel()` / `setupOtel({ logLevel })`. |
111
115
 
116
+ ## Automatic fetch instrumentation
117
+
118
+ `setupOtel()` patches `globalThis.fetch` to emit a CLIENT span per outbound request, carrying
119
+ W3C `traceparent` (and baggage) headers so traces continue across services. Each span is named by
120
+ HTTP method (`GET`, `POST`, …) and carries `http.request.method`, `url.full`, `server.address`,
121
+ `server.port`, and `http.response.status_code`; `4xx`/`5xx` responses and thrown network errors are
122
+ marked `ERROR`. This covers **outbound** requests only — inbound `Bun.serve`/Elysia server spans are
123
+ separate (see [Framework integration](#framework-integration)).
124
+
125
+ - **Default:** on when a traces endpoint is configured. Pass `instrumentFetch: false` to disable, or
126
+ `instrumentFetch: true` to force it on even without an endpoint.
127
+ - **Filter URLs:** `instrumentFetch: { ignore: (url) => url.includes("/healthz") }`. Your own OTLP
128
+ endpoint is always excluded automatically, so the exporter never traces itself.
129
+ - **Why Bun needs this:** Bun's native `fetch` emits no `diagnostics_channel` events, so
130
+ `@opentelemetry/instrumentation-undici` / `-http` — and `opentelemetry-instrumentation-fetch-node`,
131
+ which is itself `diagnostics_channel`-based — produce no spans. Wrapping the global is the only
132
+ mechanism that works, and it behaves identically on Node.
133
+ - **Caveat:** `url.full` includes the query string. If your URLs carry secrets there, use `ignore`
134
+ or redact upstream.
135
+
136
+ `setupOtel()` also installs a global W3C trace-context + baggage propagator (previously none was
137
+ registered) — that is what makes the outbound `traceparent` injection, and any manual propagation,
138
+ actually take effect.
139
+
112
140
  ## Running on Node vs Bun
113
141
 
114
142
  The same code runs unmodified on both. Pick whichever you prefer:
package/dist/index.d.ts CHANGED
@@ -1,3 +1,30 @@
1
+ interface InstrumentFetchOptions {
2
+ /**
3
+ * Return `true` to skip instrumenting a request whose absolute URL is passed
4
+ * in. Useful to drop noisy endpoints or URLs that carry secrets in their
5
+ * query string. The request is still performed — only the span is skipped.
6
+ */
7
+ ignore?: (url: string) => boolean;
8
+ }
9
+ interface FetchInstrumentation {
10
+ /** Restore the original `globalThis.fetch`. Safe to call more than once. */
11
+ unpatch(): void;
12
+ }
13
+ /**
14
+ * Wrap `globalThis.fetch` so every outbound request produces a CLIENT span and
15
+ * carries W3C trace context to the downstream service.
16
+ *
17
+ * On Bun this is the only fetch instrumentation that works: Bun's native fetch
18
+ * emits no `diagnostics_channel` events, so the standard `instrumentation-undici`
19
+ * / `instrumentation-http` (and `opentelemetry-instrumentation-fetch-node`,
20
+ * which is itself diagnostics_channel-based) produce no spans. It works
21
+ * identically on Node, where `globalThis.fetch` is undici-backed.
22
+ *
23
+ * Idempotent: a second call does not stack another wrapper. Returns a handle
24
+ * whose `unpatch()` restores the original fetch.
25
+ */
26
+ declare function instrumentFetch(options?: InstrumentFetchOptions): FetchInstrumentation;
27
+
1
28
  type LogAttrs = Record<string, string | number | boolean | undefined>;
2
29
  /**
3
30
  * Minimum severity that gets emitted (to both the OTLP record and the console).
@@ -53,6 +80,17 @@ interface SetupOtelOptions {
53
80
  * conflicts.
54
81
  */
55
82
  headers?: Record<string, string>;
83
+ /**
84
+ * Auto-instrument outbound `globalThis.fetch` with CLIENT spans and W3C
85
+ * trace-context propagation. On Bun this is the only fetch instrumentation
86
+ * that works (diagnostics_channel-based instrumentations emit nothing on
87
+ * Bun's native fetch); it works identically on Node.
88
+ *
89
+ * `true` enables with defaults; pass an object to filter URLs via `ignore`.
90
+ * Defaults to enabled when a traces endpoint is configured. Pass `false` to
91
+ * disable.
92
+ */
93
+ instrumentFetch?: boolean | InstrumentFetchOptions;
56
94
  /**
57
95
  * Minimum log level emitted by `createLogger()` (to both OTLP and console).
58
96
  * The `LOG_LEVEL` env var still takes precedence. Defaults to `debug` in
@@ -92,4 +130,4 @@ declare const PHOTON_OTEL_VERSION = "0.1.0";
92
130
  declare function withSpan<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
93
131
  declare function withSpan<T>(name: string, attrs: LogAttrs, fn: () => Promise<T> | T): Promise<T>;
94
132
 
95
- export { type LogAttrs, type LogLevel, type OtelHandle, PHOTON_OTEL_VERSION, type PhotonLogger, type SetupOtelOptions, createLogger, getLogLevel, isOtelActive, sanitizeEmail, sanitizeErrorMessage, sanitizePhone, setLogLevel, setupOtel, withSpan };
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 };
package/dist/index.js CHANGED
@@ -1,11 +1,219 @@
1
- // src/logger.ts
2
- import { context as otelContext } from "@opentelemetry/api";
3
- import { logs, SeverityNumber } from "@opentelemetry/api-logs";
1
+ // src/instrument-fetch.ts
2
+ import {
3
+ context,
4
+ propagation,
5
+ SpanKind,
6
+ SpanStatusCode,
7
+ trace
8
+ } from "@opentelemetry/api";
9
+ import {
10
+ ATTR_ERROR_TYPE,
11
+ ATTR_HTTP_REQUEST_METHOD,
12
+ ATTR_HTTP_RESPONSE_STATUS_CODE,
13
+ ATTR_SERVER_ADDRESS,
14
+ ATTR_SERVER_PORT,
15
+ ATTR_URL_FULL
16
+ } from "@opentelemetry/semantic-conventions";
17
+
18
+ // src/sanitize.ts
19
+ var PHONE_PATTERN = /\+?\d[\d\s()\-.]{6,18}\d/g;
20
+ var EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
21
+ function sanitizePhone(input) {
22
+ const hasPlus = input.startsWith("+");
23
+ const digits = input.replace(/\D/g, "");
24
+ if (digits.length < 8) {
25
+ return hasPlus ? "+xxxx" : "xxxx";
26
+ }
27
+ const head = digits.slice(0, 3);
28
+ const tail = digits.slice(-4);
29
+ const middleLength = digits.length - head.length - tail.length;
30
+ return `${hasPlus ? "+" : ""}${head}${"x".repeat(middleLength)}${tail}`;
31
+ }
32
+ function sanitizeEmail(input) {
33
+ const atIndex = input.lastIndexOf("@");
34
+ if (atIndex < 1) {
35
+ return "***";
36
+ }
37
+ const local = input.slice(0, atIndex);
38
+ const domain = input.slice(atIndex + 1);
39
+ const dotIndex = domain.lastIndexOf(".");
40
+ if (dotIndex < 1) {
41
+ return "***";
42
+ }
43
+ const localHead = local.slice(0, 2);
44
+ const domainHead = domain.slice(0, 1);
45
+ const tld = domain.slice(dotIndex);
46
+ return `${localHead}***@${domainHead}***${tld}`;
47
+ }
48
+ function sanitizeErrorMessage(input) {
49
+ return input.replace(EMAIL_PATTERN, (match) => sanitizeEmail(match)).replace(PHONE_PATTERN, (match) => sanitizePhone(match));
50
+ }
4
51
 
5
52
  // src/version.ts
6
53
  var PHOTON_OTEL_VERSION = "0.1.0";
7
54
 
55
+ // src/instrument-fetch.ts
56
+ var PATCH_MARKER = /* @__PURE__ */ Symbol.for("@photon-ai/otel.fetch.original");
57
+ var HTTP_ERROR_STATUS_MIN = 400;
58
+ var DEFAULT_PORTS = { "https:": 443, "http:": 80 };
59
+ var scopedTracer;
60
+ function getTracer() {
61
+ if (!scopedTracer) {
62
+ scopedTracer = trace.getTracer("@photon-ai/otel", PHOTON_OTEL_VERSION);
63
+ }
64
+ return scopedTracer;
65
+ }
66
+ function setGlobalFetch(fn) {
67
+ globalThis.fetch = fn;
68
+ }
69
+ function getPatchOriginal(fn) {
70
+ return fn[PATCH_MARKER];
71
+ }
72
+ function setPatchOriginal(fn, original) {
73
+ fn[PATCH_MARKER] = original;
74
+ }
75
+ function preserveProps(from, to) {
76
+ for (const key of Object.getOwnPropertyNames(from)) {
77
+ if (key in to) {
78
+ continue;
79
+ }
80
+ const descriptor = Object.getOwnPropertyDescriptor(from, key);
81
+ if (descriptor) {
82
+ Object.defineProperty(to, key, descriptor);
83
+ }
84
+ }
85
+ }
86
+ function resolveRequestMeta(input, init) {
87
+ if (input instanceof Request) {
88
+ return { method: input.method, url: input.url };
89
+ }
90
+ const url = typeof input === "string" ? input : input.toString();
91
+ return { method: init?.method ?? "GET", url };
92
+ }
93
+ function resolvePort(parsed) {
94
+ if (parsed.port) {
95
+ return Number(parsed.port);
96
+ }
97
+ return DEFAULT_PORTS[parsed.protocol];
98
+ }
99
+ function toAttributes(attrs) {
100
+ const out = {};
101
+ for (const [key, value] of Object.entries(attrs)) {
102
+ if (value !== void 0) {
103
+ out[key] = value;
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+ function fetchAttributes(method, url) {
109
+ const attrs = {
110
+ [ATTR_HTTP_REQUEST_METHOD]: method,
111
+ [ATTR_URL_FULL]: url
112
+ };
113
+ try {
114
+ const parsed = new URL(url);
115
+ attrs[ATTR_SERVER_ADDRESS] = parsed.hostname || void 0;
116
+ attrs[ATTR_SERVER_PORT] = resolvePort(parsed);
117
+ } catch {
118
+ }
119
+ return toAttributes(attrs);
120
+ }
121
+ function buildPropagatedHeaders(input, init) {
122
+ const headers = new Headers(
123
+ input instanceof Request ? input.headers : void 0
124
+ );
125
+ if (init?.headers) {
126
+ for (const [key, value] of new Headers(init.headers).entries()) {
127
+ headers.set(key, value);
128
+ }
129
+ }
130
+ propagation.inject(context.active(), headers, {
131
+ set: (carrier, key, value) => {
132
+ carrier.set(key, value);
133
+ }
134
+ });
135
+ return headers;
136
+ }
137
+ function callOriginal(original, input, init, headers) {
138
+ if (input instanceof Request) {
139
+ if (input.bodyUsed) {
140
+ for (const [key, value] of headers.entries()) {
141
+ input.headers.set(key, value);
142
+ }
143
+ return original(input, init);
144
+ }
145
+ return original(new Request(input, { ...init, headers }));
146
+ }
147
+ return original(input, { ...init, headers });
148
+ }
149
+ function buildWrappedFetch(original, options) {
150
+ return (input, init) => {
151
+ const { method, url } = resolveRequestMeta(input, init);
152
+ if (options?.ignore?.(url)) {
153
+ return original(input, init);
154
+ }
155
+ const name = method.toUpperCase();
156
+ return getTracer().startActiveSpan(
157
+ name,
158
+ { kind: SpanKind.CLIENT },
159
+ async (span) => {
160
+ span.setAttributes(fetchAttributes(name, url));
161
+ try {
162
+ const headers = buildPropagatedHeaders(input, init);
163
+ const response = await callOriginal(original, input, init, headers);
164
+ span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
165
+ span.setStatus({
166
+ code: response.status >= HTTP_ERROR_STATUS_MIN ? SpanStatusCode.ERROR : SpanStatusCode.OK
167
+ });
168
+ return response;
169
+ } catch (err) {
170
+ span.recordException(err);
171
+ const errorObj = err instanceof Error ? err : void 0;
172
+ span.setAttribute(
173
+ ATTR_ERROR_TYPE,
174
+ errorObj?.constructor.name ?? typeof err
175
+ );
176
+ span.setStatus({
177
+ code: SpanStatusCode.ERROR,
178
+ message: errorObj ? sanitizeErrorMessage(errorObj.message) : sanitizeErrorMessage(String(err))
179
+ });
180
+ throw err;
181
+ } finally {
182
+ span.end();
183
+ }
184
+ }
185
+ );
186
+ };
187
+ }
188
+ function instrumentFetch(options) {
189
+ const current = globalThis.fetch;
190
+ const existingOriginal = getPatchOriginal(current);
191
+ if (existingOriginal) {
192
+ return {
193
+ unpatch() {
194
+ if (globalThis.fetch === current) {
195
+ setGlobalFetch(existingOriginal);
196
+ }
197
+ }
198
+ };
199
+ }
200
+ const original = current;
201
+ const wrapped = buildWrappedFetch(original, options);
202
+ preserveProps(original, wrapped);
203
+ setPatchOriginal(wrapped, original);
204
+ setGlobalFetch(wrapped);
205
+ return {
206
+ unpatch() {
207
+ if (globalThis.fetch === wrapped) {
208
+ setGlobalFetch(original);
209
+ }
210
+ }
211
+ };
212
+ }
213
+
8
214
  // src/logger.ts
215
+ import { context as otelContext } from "@opentelemetry/api";
216
+ import { logs, SeverityNumber } from "@opentelemetry/api-logs";
9
217
  var LEVEL_SEVERITY = {
10
218
  debug: SeverityNumber.DEBUG,
11
219
  // 5
@@ -112,44 +320,15 @@ function createLogger(module) {
112
320
  };
113
321
  }
114
322
 
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}`;
128
- }
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}`;
144
- }
145
- function sanitizeErrorMessage(input) {
146
- return input.replace(EMAIL_PATTERN, (match) => sanitizeEmail(match)).replace(PHONE_PATTERN, (match) => sanitizePhone(match));
147
- }
148
-
149
323
  // src/setup.ts
150
- import { context, trace } from "@opentelemetry/api";
324
+ import { context as context2, propagation as propagation2, trace as trace2 } from "@opentelemetry/api";
151
325
  import { logs as logs2 } from "@opentelemetry/api-logs";
152
326
  import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
327
+ import {
328
+ CompositePropagator,
329
+ W3CBaggagePropagator,
330
+ W3CTraceContextPropagator
331
+ } from "@opentelemetry/core";
153
332
  import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
154
333
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
155
334
  import { resourceFromAttributes } from "@opentelemetry/resources";
@@ -197,6 +376,42 @@ function resolveLogsEndpoint(base) {
197
376
  const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;
198
377
  return generic ? `${generic.replace(TRAILING_SLASH, "")}/v1/logs` : void 0;
199
378
  }
379
+ function otlpEndpointKey(url) {
380
+ try {
381
+ const parsed = new URL(url);
382
+ return `${parsed.origin}${parsed.pathname.replace(TRAILING_SLASH, "")}`;
383
+ } catch {
384
+ return;
385
+ }
386
+ }
387
+ function otlpEndpointKeysOf(tracesEndpoint, logsEndpoint) {
388
+ const keys = [];
389
+ for (const endpoint of [tracesEndpoint, logsEndpoint]) {
390
+ if (!endpoint) {
391
+ continue;
392
+ }
393
+ const key = otlpEndpointKey(endpoint);
394
+ if (key) {
395
+ keys.push(key);
396
+ }
397
+ }
398
+ return keys;
399
+ }
400
+ function startFetchInstrumentation(option, hasTraces, tracesEndpoint, logsEndpoint) {
401
+ const want = option ?? hasTraces;
402
+ if (!want) {
403
+ return;
404
+ }
405
+ const userOptions = typeof option === "object" ? option : void 0;
406
+ const otlpEndpointKeys = otlpEndpointKeysOf(tracesEndpoint, logsEndpoint);
407
+ return instrumentFetch({
408
+ ignore: (url) => {
409
+ const key = otlpEndpointKey(url);
410
+ const isOtlpEndpoint = key !== void 0 && otlpEndpointKeys.includes(key);
411
+ return isOtlpEndpoint || (userOptions?.ignore?.(url) ?? false);
412
+ }
413
+ });
414
+ }
200
415
  function setupOtel(options) {
201
416
  if (activeHandle) {
202
417
  return activeHandle;
@@ -217,7 +432,15 @@ function setupOtel(options) {
217
432
  "deployment.environment": process.env.DEPLOYMENT_ENV ?? "development",
218
433
  ...options.resourceAttributes
219
434
  });
220
- context.setGlobalContextManager(new AsyncLocalStorageContextManager());
435
+ context2.setGlobalContextManager(new AsyncLocalStorageContextManager());
436
+ propagation2.setGlobalPropagator(
437
+ new CompositePropagator({
438
+ propagators: [
439
+ new W3CTraceContextPropagator(),
440
+ new W3CBaggagePropagator()
441
+ ]
442
+ })
443
+ );
221
444
  const traceProcessors = tracesEndpoint ? [
222
445
  new BatchSpanProcessor(
223
446
  new OTLPTraceExporter({
@@ -230,7 +453,13 @@ function setupOtel(options) {
230
453
  resource,
231
454
  spanProcessors: traceProcessors
232
455
  });
233
- trace.setGlobalTracerProvider(tracerProvider);
456
+ trace2.setGlobalTracerProvider(tracerProvider);
457
+ const fetchInstrumentation = startFetchInstrumentation(
458
+ options.instrumentFetch,
459
+ traceProcessors.length > 0,
460
+ tracesEndpoint,
461
+ logsEndpoint
462
+ );
234
463
  const logProcessors = logsEndpoint ? [
235
464
  new BatchLogRecordProcessor(
236
465
  new OTLPLogExporter({
@@ -246,6 +475,7 @@ function setupOtel(options) {
246
475
  logs2.setGlobalLoggerProvider(loggerProvider);
247
476
  const handle = {
248
477
  async shutdown() {
478
+ fetchInstrumentation?.unpatch();
249
479
  await Promise.allSettled([
250
480
  tracerProvider.shutdown(),
251
481
  loggerProvider.shutdown()
@@ -262,17 +492,17 @@ function isOtelActive() {
262
492
 
263
493
  // src/with-span.ts
264
494
  import {
265
- SpanStatusCode,
266
- trace as trace2
495
+ SpanStatusCode as SpanStatusCode2,
496
+ trace as trace3
267
497
  } from "@opentelemetry/api";
268
- var scopedTracer;
269
- function getTracer() {
270
- if (!scopedTracer) {
271
- scopedTracer = trace2.getTracer("@photon-ai/otel", PHOTON_OTEL_VERSION);
498
+ var scopedTracer2;
499
+ function getTracer2() {
500
+ if (!scopedTracer2) {
501
+ scopedTracer2 = trace3.getTracer("@photon-ai/otel", PHOTON_OTEL_VERSION);
272
502
  }
273
- return scopedTracer;
503
+ return scopedTracer2;
274
504
  }
275
- function toAttributes(attrs) {
505
+ function toAttributes2(attrs) {
276
506
  const out = {};
277
507
  for (const [k, v] of Object.entries(attrs)) {
278
508
  if (v !== void 0) {
@@ -287,20 +517,20 @@ function withSpan(name, attrsOrFn, maybeFn) {
287
517
  throw new Error("withSpan: function argument is required");
288
518
  }
289
519
  const attrs = typeof attrsOrFn === "function" ? void 0 : attrsOrFn;
290
- return getTracer().startActiveSpan(name, async (span) => {
520
+ return getTracer2().startActiveSpan(name, async (span) => {
291
521
  if (attrs) {
292
- span.setAttributes(toAttributes(attrs));
522
+ span.setAttributes(toAttributes2(attrs));
293
523
  }
294
524
  try {
295
525
  const result = await fn();
296
- span.setStatus({ code: SpanStatusCode.OK });
526
+ span.setStatus({ code: SpanStatusCode2.OK });
297
527
  return result;
298
528
  } catch (err) {
299
529
  span.recordException(err);
300
530
  const errorObj = err instanceof Error ? err : void 0;
301
531
  span.setAttribute("error.type", errorObj?.constructor.name ?? typeof err);
302
532
  span.setStatus({
303
- code: SpanStatusCode.ERROR,
533
+ code: SpanStatusCode2.ERROR,
304
534
  message: errorObj ? sanitizeErrorMessage(errorObj.message) : sanitizeErrorMessage(String(err))
305
535
  });
306
536
  throw err;
@@ -313,6 +543,7 @@ export {
313
543
  PHOTON_OTEL_VERSION,
314
544
  createLogger,
315
545
  getLogLevel,
546
+ instrumentFetch,
316
547
  isOtelActive,
317
548
  sanitizeEmail,
318
549
  sanitizeErrorMessage,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/logger.ts","../src/version.ts","../src/sanitize.ts","../src/setup.ts","../src/with-span.ts"],"sourcesContent":["import { context as otelContext } from \"@opentelemetry/api\";\nimport { type Logger, logs, SeverityNumber } from \"@opentelemetry/api-logs\";\nimport { PHOTON_OTEL_VERSION } from \"./version\";\n\nexport type LogAttrs = Record<string, string | number | boolean | undefined>;\n\n/**\n * Minimum severity that gets emitted (to both the OTLP record and the console).\n * `\"silent\"` suppresses everything, including errors.\n */\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"silent\";\n\nconst LEVEL_SEVERITY: Record<LogLevel, number> = {\n debug: SeverityNumber.DEBUG, // 5\n info: SeverityNumber.INFO, // 9\n warn: SeverityNumber.WARN, // 13\n error: SeverityNumber.ERROR, // 17\n silent: Number.POSITIVE_INFINITY,\n};\n\nlet levelOverride: LogLevel | undefined;\n\nfunction envLevel(): LogLevel | undefined {\n const raw = process.env.LOG_LEVEL?.toLowerCase();\n if (raw && raw in LEVEL_SEVERITY) {\n return raw as LogLevel;\n }\n return;\n}\n\nfunction defaultLevel(): LogLevel {\n return (process.env.DEPLOYMENT_ENV ?? \"development\") === \"development\"\n ? \"debug\"\n : \"info\";\n}\n\n/**\n * Resolve the active level fresh on each call so that `LOG_LEVEL` changes and\n * `setLogLevel()` both take effect immediately. Resolution order (env wins, to\n * match the rest of the package's config story):\n * 1. `LOG_LEVEL` env var\n * 2. `setLogLevel()` / `setupOtel({ logLevel })`\n * 3. environment-driven default (`debug` in development, `info` otherwise)\n */\nfunction resolveLevel(): LogLevel {\n return envLevel() ?? levelOverride ?? defaultLevel();\n}\n\n/**\n * Programmatically set the minimum log level. Takes effect immediately for\n * subsequent logs. `LOG_LEVEL` env var still wins if set.\n */\nexport function setLogLevel(level: LogLevel): void {\n levelOverride = level;\n}\n\n/** Current effective log level, after env / override / default resolution. */\nexport function getLogLevel(): LogLevel {\n return resolveLevel();\n}\n\nlet scopedLogger: Logger | undefined;\n\nfunction getLogger(): Logger {\n if (!scopedLogger) {\n scopedLogger = logs.getLogger(\"@photon-ai/otel\", PHOTON_OTEL_VERSION);\n }\n return scopedLogger;\n}\n\nfunction filterUndefined(\n attrs?: LogAttrs\n): Record<string, string | number | boolean> {\n if (!attrs) {\n return {};\n }\n const out: Record<string, string | number | boolean> = {};\n for (const [k, v] of Object.entries(attrs)) {\n if (v !== undefined) {\n out[k] = v;\n }\n }\n return out;\n}\n\nfunction consoleFor(\n severityNumber: SeverityNumber\n): (...args: unknown[]) => void {\n if (severityNumber >= SeverityNumber.ERROR) {\n return console.error;\n }\n if (severityNumber >= SeverityNumber.WARN) {\n return console.warn;\n }\n if (severityNumber >= SeverityNumber.INFO) {\n return console.info;\n }\n return console.debug;\n}\n\nfunction emit(\n severityNumber: SeverityNumber,\n severityText: string,\n module: string,\n message: string,\n attrs?: LogAttrs,\n error?: unknown\n): void {\n // Single gate: drop sub-threshold logs before they reach OTLP or the console.\n if (severityNumber < LEVEL_SEVERITY[resolveLevel()]) {\n return;\n }\n\n const userAttrs = filterUndefined(attrs);\n const attributes: Record<string, string | number | boolean> = {\n \"log.module\": module,\n ...userAttrs,\n };\n\n if (error instanceof Error) {\n attributes[\"exception.type\"] = error.name;\n attributes[\"exception.message\"] = error.message;\n if (error.stack) {\n attributes[\"exception.stacktrace\"] = error.stack;\n }\n } else if (error !== undefined) {\n // Don't silently drop non-Error throws (strings, plain objects, etc.).\n attributes[\"exception.type\"] = typeof error;\n attributes[\"exception.message\"] = String(error);\n }\n\n getLogger().emit({\n severityNumber,\n severityText,\n body: message,\n attributes,\n context: otelContext.active(),\n });\n\n // Console: `[module] LEVEL message { ...attrs }` plus the raw error so the\n // runtime renders the full stack and pretty-prints the attribute bag.\n const extras: unknown[] = [];\n if (Object.keys(userAttrs).length > 0) {\n extras.push(userAttrs);\n }\n if (error !== undefined) {\n extras.push(error);\n }\n consoleFor(severityNumber)(`[${module}]`, severityText, message, ...extras);\n}\n\nexport interface PhotonLogger {\n debug(message: string, attrs?: LogAttrs, error?: unknown): void;\n error(message: string, attrs?: LogAttrs, error?: unknown): void;\n info(message: string, attrs?: LogAttrs, error?: unknown): void;\n warn(message: string, attrs?: LogAttrs, error?: unknown): void;\n}\n\nexport function createLogger(module: string): PhotonLogger {\n return {\n debug: (message, attrs, error) =>\n emit(SeverityNumber.DEBUG, \"DEBUG\", module, message, attrs, error),\n info: (message, attrs, error) =>\n emit(SeverityNumber.INFO, \"INFO\", module, message, attrs, error),\n warn: (message, attrs, error) =>\n emit(SeverityNumber.WARN, \"WARN\", module, message, attrs, error),\n error: (message, attrs, error) =>\n emit(SeverityNumber.ERROR, \"ERROR\", module, message, attrs, error),\n };\n}\n","export const PHOTON_OTEL_VERSION = \"0.1.0\";\n","// E.164-ish phone match: optional `+`, 7–15 digits with optional separators.\nconst PHONE_PATTERN = /\\+?\\d[\\d\\s()\\-.]{6,18}\\d/g;\nconst EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g;\n\n/**\n * Mask a phone number, keeping the leading `+` (if any) plus the first 3 digits\n * and the last 4 digits visible. Example: `+13315553374` -> `+133xxxxx3374`.\n *\n * Inputs that don't have enough digits to safely mask are returned as\n * `xxxx` to avoid leaking the entire short value.\n */\nexport function sanitizePhone(input: string): string {\n const hasPlus = input.startsWith(\"+\");\n const digits = input.replace(/\\D/g, \"\");\n if (digits.length < 8) {\n return hasPlus ? \"+xxxx\" : \"xxxx\";\n }\n const head = digits.slice(0, 3);\n const tail = digits.slice(-4);\n const middleLength = digits.length - head.length - tail.length;\n return `${hasPlus ? \"+\" : \"\"}${head}${\"x\".repeat(middleLength)}${tail}`;\n}\n\n/**\n * Mask an email address, keeping the first 2 chars of the local part, the\n * first char of the domain, and the TLD. Example:\n * `foo.bar@example.com` -> `fo***@e***.com`.\n */\nexport function sanitizeEmail(input: string): string {\n const atIndex = input.lastIndexOf(\"@\");\n if (atIndex < 1) {\n return \"***\";\n }\n const local = input.slice(0, atIndex);\n const domain = input.slice(atIndex + 1);\n const dotIndex = domain.lastIndexOf(\".\");\n if (dotIndex < 1) {\n return \"***\";\n }\n const localHead = local.slice(0, 2);\n const domainHead = domain.slice(0, 1);\n const tld = domain.slice(dotIndex);\n return `${localHead}***@${domainHead}***${tld}`;\n}\n\n/**\n * Replace every phone number and email address inside a free-form string with\n * its sanitized form. Used to scrub `Error.message` values before attaching\n * them to span status.\n */\nexport function sanitizeErrorMessage(input: string): string {\n return input\n .replace(EMAIL_PATTERN, (match) => sanitizeEmail(match))\n .replace(PHONE_PATTERN, (match) => sanitizePhone(match));\n}\n","import { context, trace } from \"@opentelemetry/api\";\nimport { logs } from \"@opentelemetry/api-logs\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n BatchLogRecordProcessor,\n LoggerProvider,\n} from \"@opentelemetry/sdk-logs\";\nimport {\n BasicTracerProvider,\n BatchSpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { type LogLevel, setLogLevel } from \"./logger\";\n\nexport interface SetupOtelOptions {\n /**\n * Default OTLP/HTTP base endpoint (e.g. `https://otel.example.com`). The\n * `/v1/traces` and `/v1/logs` paths are appended automatically. Standard\n * `OTEL_EXPORTER_OTLP_*` env vars always take precedence.\n */\n endpoint?: string;\n /**\n * Default OTLP headers (e.g. `{ Authorization: \"Basic ...\" }`). Merged with\n * any headers parsed from `OTEL_EXPORTER_OTLP_HEADERS`; env values win on\n * conflicts.\n */\n headers?: Record<string, string>;\n /**\n * Minimum log level emitted by `createLogger()` (to both OTLP and console).\n * The `LOG_LEVEL` env var still takes precedence. Defaults to `debug` in\n * development and `info` otherwise.\n */\n logLevel?: LogLevel;\n /**\n * Extra resource attributes attached to every span/log alongside\n * `service.name` / `service.version`.\n */\n resourceAttributes?: Record<string, string | number | boolean>;\n serviceName: string;\n serviceVersion?: string;\n}\n\nexport interface OtelHandle {\n shutdown(): Promise<void>;\n}\n\nlet activeHandle: OtelHandle | undefined;\n\nconst TRAILING_SLASH = /\\/$/;\n\nfunction parseEnvHeaders(raw: string | undefined): Record<string, string> {\n if (!raw) {\n return {};\n }\n const out: Record<string, string> = {};\n for (const pair of raw.split(\",\")) {\n const eq = pair.indexOf(\"=\");\n if (eq <= 0) {\n continue;\n }\n const key = pair.slice(0, eq).trim();\n const value = pair.slice(eq + 1).trim();\n if (key) {\n out[key] = value;\n }\n }\n return out;\n}\n\nfunction resolveTracesEndpoint(base: string | undefined): string | undefined {\n const traces = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;\n if (traces) {\n return traces;\n }\n const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;\n return generic\n ? `${generic.replace(TRAILING_SLASH, \"\")}/v1/traces`\n : undefined;\n}\n\nfunction resolveLogsEndpoint(base: string | undefined): string | undefined {\n const logsEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;\n if (logsEndpoint) {\n return logsEndpoint;\n }\n const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;\n return generic ? `${generic.replace(TRAILING_SLASH, \"\")}/v1/logs` : undefined;\n}\n\n/**\n * Boot an OTLP/HTTP-based OpenTelemetry pipeline (traces + logs).\n *\n * Idempotent: calling twice in the same process is a no-op on the second\n * call, so libraries can safely invoke this without clobbering an app-level\n * OTel setup that ran earlier.\n *\n * Standard `OTEL_EXPORTER_OTLP_*` env vars override the `endpoint` and\n * `headers` arguments — this matches the OpenTelemetry SDK config spec.\n */\nexport function setupOtel(options: SetupOtelOptions): OtelHandle {\n if (activeHandle) {\n return activeHandle;\n }\n\n if (options.logLevel) {\n setLogLevel(options.logLevel);\n }\n\n const tracesEndpoint = resolveTracesEndpoint(options.endpoint);\n const logsEndpoint = resolveLogsEndpoint(options.endpoint);\n const mergedHeaders = {\n ...options.headers,\n ...parseEnvHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS),\n };\n const hasHeaders = Object.keys(mergedHeaders).length > 0;\n\n const resource = resourceFromAttributes({\n \"service.name\": options.serviceName,\n ...(options.serviceVersion\n ? { \"service.version\": options.serviceVersion }\n : {}),\n \"deployment.environment\": process.env.DEPLOYMENT_ENV ?? \"development\",\n ...options.resourceAttributes,\n });\n\n context.setGlobalContextManager(new AsyncLocalStorageContextManager());\n\n const traceProcessors = tracesEndpoint\n ? [\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: tracesEndpoint,\n headers: hasHeaders ? mergedHeaders : undefined,\n })\n ),\n ]\n : [];\n\n const tracerProvider = new BasicTracerProvider({\n resource,\n spanProcessors: traceProcessors,\n });\n trace.setGlobalTracerProvider(tracerProvider);\n\n const logProcessors = logsEndpoint\n ? [\n new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: logsEndpoint,\n headers: hasHeaders ? mergedHeaders : undefined,\n })\n ),\n ]\n : [];\n\n const loggerProvider = new LoggerProvider({\n resource,\n processors: logProcessors,\n });\n logs.setGlobalLoggerProvider(loggerProvider);\n\n const handle: OtelHandle = {\n async shutdown() {\n await Promise.allSettled([\n tracerProvider.shutdown(),\n loggerProvider.shutdown(),\n ]);\n activeHandle = undefined;\n },\n };\n\n activeHandle = handle;\n return handle;\n}\n\n/**\n * Read-only accessor for tests / debug paths that need to know whether\n * `setupOtel` has already run in this process.\n */\nexport function isOtelActive(): boolean {\n return activeHandle !== undefined;\n}\n","import {\n type Attributes,\n SpanStatusCode,\n type Tracer,\n trace,\n} from \"@opentelemetry/api\";\nimport type { LogAttrs } from \"./logger\";\nimport { sanitizeErrorMessage } from \"./sanitize\";\nimport { PHOTON_OTEL_VERSION } from \"./version\";\n\nlet scopedTracer: Tracer | undefined;\n\nfunction getTracer(): Tracer {\n if (!scopedTracer) {\n scopedTracer = trace.getTracer(\"@photon-ai/otel\", PHOTON_OTEL_VERSION);\n }\n return scopedTracer;\n}\n\nfunction toAttributes(attrs: LogAttrs): Attributes {\n const out: Attributes = {};\n for (const [k, v] of Object.entries(attrs)) {\n if (v !== undefined) {\n out[k] = v;\n }\n }\n return out;\n}\n\nexport function withSpan<T>(name: string, fn: () => Promise<T> | T): Promise<T>;\nexport function withSpan<T>(\n name: string,\n attrs: LogAttrs,\n fn: () => Promise<T> | T\n): Promise<T>;\nexport function withSpan<T>(\n name: string,\n attrsOrFn: LogAttrs | (() => Promise<T> | T),\n maybeFn?: () => Promise<T> | T\n): Promise<T> {\n const fn = typeof attrsOrFn === \"function\" ? attrsOrFn : maybeFn;\n if (!fn) {\n throw new Error(\"withSpan: function argument is required\");\n }\n const attrs = typeof attrsOrFn === \"function\" ? undefined : attrsOrFn;\n\n return getTracer().startActiveSpan(name, async (span) => {\n if (attrs) {\n span.setAttributes(toAttributes(attrs));\n }\n try {\n const result = await fn();\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (err) {\n span.recordException(err as Error);\n const errorObj = err instanceof Error ? err : undefined;\n span.setAttribute(\"error.type\", errorObj?.constructor.name ?? typeof err);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: errorObj\n ? sanitizeErrorMessage(errorObj.message)\n : sanitizeErrorMessage(String(err)),\n });\n throw err;\n } finally {\n span.end();\n }\n });\n}\n"],"mappings":"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","names":["logs","logs","trace","trace"]}
1
+ {"version":3,"sources":["../src/instrument-fetch.ts","../src/sanitize.ts","../src/version.ts","../src/logger.ts","../src/setup.ts","../src/with-span.ts"],"sourcesContent":["import {\n type Attributes,\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n type Tracer,\n trace,\n} from \"@opentelemetry/api\";\nimport {\n ATTR_ERROR_TYPE,\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT,\n ATTR_URL_FULL,\n} from \"@opentelemetry/semantic-conventions\";\nimport { sanitizeErrorMessage } from \"./sanitize\";\nimport { PHOTON_OTEL_VERSION } from \"./version\";\n\nexport interface InstrumentFetchOptions {\n /**\n * Return `true` to skip instrumenting a request whose absolute URL is passed\n * in. Useful to drop noisy endpoints or URLs that carry secrets in their\n * query string. The request is still performed — only the span is skipped.\n */\n ignore?: (url: string) => boolean;\n}\n\nexport interface FetchInstrumentation {\n /** Restore the original `globalThis.fetch`. Safe to call more than once. */\n unpatch(): void;\n}\n\ntype FetchInput = Parameters<typeof fetch>[0];\ntype FetchInit = Parameters<typeof fetch>[1];\ntype FetchFn = (input: FetchInput, init?: FetchInit) => Promise<Response>;\n\n/**\n * Stored on the wrapper via the global symbol registry (`Symbol.for`) so the\n * double-wrap guard holds even when two copies of this module load — which can\n * happen because the `bun` export condition serves `src/` while `default`\n * serves `dist/`.\n */\nconst PATCH_MARKER = Symbol.for(\"@photon-ai/otel.fetch.original\");\n\nconst HTTP_ERROR_STATUS_MIN = 400;\nconst DEFAULT_PORTS: Record<string, number> = { \"https:\": 443, \"http:\": 80 };\n\nlet scopedTracer: Tracer | undefined;\n\nfunction getTracer(): Tracer {\n if (!scopedTracer) {\n scopedTracer = trace.getTracer(\"@photon-ai/otel\", PHOTON_OTEL_VERSION);\n }\n return scopedTracer;\n}\n\nfunction setGlobalFetch(fn: FetchFn): void {\n // `preconnect` (Bun) is copied onto wrappers by preserveProps; the cast just\n // tells TypeScript the runtime object satisfies the full `fetch` type.\n globalThis.fetch = fn as typeof fetch;\n}\n\nfunction getPatchOriginal(fn: FetchFn): FetchFn | undefined {\n return (fn as unknown as Record<symbol, FetchFn | undefined>)[PATCH_MARKER];\n}\n\nfunction setPatchOriginal(fn: FetchFn, original: FetchFn): void {\n (fn as unknown as Record<symbol, FetchFn>)[PATCH_MARKER] = original;\n}\n\n/** Copy extra own properties (e.g. Bun's `fetch.preconnect`) onto the wrapper. */\nfunction preserveProps(from: FetchFn, to: FetchFn): void {\n for (const key of Object.getOwnPropertyNames(from)) {\n if (key in to) {\n continue;\n }\n const descriptor = Object.getOwnPropertyDescriptor(from, key);\n if (descriptor) {\n Object.defineProperty(to, key, descriptor);\n }\n }\n}\n\nfunction resolveRequestMeta(\n input: FetchInput,\n init: FetchInit\n): { method: string; url: string } {\n if (input instanceof Request) {\n return { method: input.method, url: input.url };\n }\n const url = typeof input === \"string\" ? input : input.toString();\n return { method: init?.method ?? \"GET\", url };\n}\n\nfunction resolvePort(parsed: URL): number | undefined {\n if (parsed.port) {\n return Number(parsed.port);\n }\n return DEFAULT_PORTS[parsed.protocol];\n}\n\nfunction toAttributes(attrs: Attributes): Attributes {\n const out: Attributes = {};\n for (const [key, value] of Object.entries(attrs)) {\n if (value !== undefined) {\n out[key] = value;\n }\n }\n return out;\n}\n\nfunction fetchAttributes(method: string, url: string): Attributes {\n const attrs: Attributes = {\n [ATTR_HTTP_REQUEST_METHOD]: method,\n [ATTR_URL_FULL]: url,\n };\n try {\n const parsed = new URL(url);\n attrs[ATTR_SERVER_ADDRESS] = parsed.hostname || undefined;\n attrs[ATTR_SERVER_PORT] = resolvePort(parsed);\n } catch {\n // Unparseable URL: leave server.* unset rather than failing the request.\n }\n return toAttributes(attrs);\n}\n\n/** Build the outgoing headers and inject the active trace context into them. */\nfunction buildPropagatedHeaders(input: FetchInput, init: FetchInit): Headers {\n const headers = new Headers(\n input instanceof Request ? input.headers : undefined\n );\n if (init?.headers) {\n for (const [key, value] of new Headers(init.headers).entries()) {\n headers.set(key, value);\n }\n }\n propagation.inject(context.active(), headers, {\n set: (carrier, key, value) => {\n carrier.set(key, value);\n },\n });\n return headers;\n}\n\nfunction callOriginal(\n original: FetchFn,\n input: FetchInput,\n init: FetchInit,\n headers: Headers\n): Promise<Response> {\n if (input instanceof Request) {\n // A consumed Request's body can't be rebuilt, but its headers stay mutable\n // (verified on both Bun and Node) — inject the propagated context in place\n // so trace headers still flow without reconstructing the unusable Request.\n if (input.bodyUsed) {\n for (const [key, value] of headers.entries()) {\n input.headers.set(key, value);\n }\n return original(input, init);\n }\n return original(new Request(input, { ...init, headers }));\n }\n return original(input, { ...init, headers });\n}\n\nfunction buildWrappedFetch(\n original: FetchFn,\n options?: InstrumentFetchOptions\n): FetchFn {\n return (input, init) => {\n const { method, url } = resolveRequestMeta(input, init);\n if (options?.ignore?.(url)) {\n return original(input, init);\n }\n const name = method.toUpperCase();\n return getTracer().startActiveSpan(\n name,\n { kind: SpanKind.CLIENT },\n async (span) => {\n span.setAttributes(fetchAttributes(name, url));\n try {\n const headers = buildPropagatedHeaders(input, init);\n const response = await callOriginal(original, input, init, headers);\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);\n span.setStatus({\n code:\n response.status >= HTTP_ERROR_STATUS_MIN\n ? SpanStatusCode.ERROR\n : SpanStatusCode.OK,\n });\n return response;\n } catch (err) {\n span.recordException(err as Error);\n const errorObj = err instanceof Error ? err : undefined;\n span.setAttribute(\n ATTR_ERROR_TYPE,\n errorObj?.constructor.name ?? typeof err\n );\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: errorObj\n ? sanitizeErrorMessage(errorObj.message)\n : sanitizeErrorMessage(String(err)),\n });\n throw err;\n } finally {\n span.end();\n }\n }\n );\n };\n}\n\n/**\n * Wrap `globalThis.fetch` so every outbound request produces a CLIENT span and\n * carries W3C trace context to the downstream service.\n *\n * On Bun this is the only fetch instrumentation that works: Bun's native fetch\n * emits no `diagnostics_channel` events, so the standard `instrumentation-undici`\n * / `instrumentation-http` (and `opentelemetry-instrumentation-fetch-node`,\n * which is itself diagnostics_channel-based) produce no spans. It works\n * identically on Node, where `globalThis.fetch` is undici-backed.\n *\n * Idempotent: a second call does not stack another wrapper. Returns a handle\n * whose `unpatch()` restores the original fetch.\n */\nexport function instrumentFetch(\n options?: InstrumentFetchOptions\n): FetchInstrumentation {\n const current: FetchFn = globalThis.fetch;\n const existingOriginal = getPatchOriginal(current);\n if (existingOriginal) {\n return {\n unpatch() {\n if (globalThis.fetch === current) {\n setGlobalFetch(existingOriginal);\n }\n },\n };\n }\n\n const original = current;\n const wrapped = buildWrappedFetch(original, options);\n preserveProps(original, wrapped);\n setPatchOriginal(wrapped, original);\n setGlobalFetch(wrapped);\n\n return {\n unpatch() {\n if (globalThis.fetch === wrapped) {\n setGlobalFetch(original);\n }\n },\n };\n}\n","// E.164-ish phone match: optional `+`, 7–15 digits with optional separators.\nconst PHONE_PATTERN = /\\+?\\d[\\d\\s()\\-.]{6,18}\\d/g;\nconst EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g;\n\n/**\n * Mask a phone number, keeping the leading `+` (if any) plus the first 3 digits\n * and the last 4 digits visible. Example: `+13315553374` -> `+133xxxxx3374`.\n *\n * Inputs that don't have enough digits to safely mask are returned as\n * `xxxx` to avoid leaking the entire short value.\n */\nexport function sanitizePhone(input: string): string {\n const hasPlus = input.startsWith(\"+\");\n const digits = input.replace(/\\D/g, \"\");\n if (digits.length < 8) {\n return hasPlus ? \"+xxxx\" : \"xxxx\";\n }\n const head = digits.slice(0, 3);\n const tail = digits.slice(-4);\n const middleLength = digits.length - head.length - tail.length;\n return `${hasPlus ? \"+\" : \"\"}${head}${\"x\".repeat(middleLength)}${tail}`;\n}\n\n/**\n * Mask an email address, keeping the first 2 chars of the local part, the\n * first char of the domain, and the TLD. Example:\n * `foo.bar@example.com` -> `fo***@e***.com`.\n */\nexport function sanitizeEmail(input: string): string {\n const atIndex = input.lastIndexOf(\"@\");\n if (atIndex < 1) {\n return \"***\";\n }\n const local = input.slice(0, atIndex);\n const domain = input.slice(atIndex + 1);\n const dotIndex = domain.lastIndexOf(\".\");\n if (dotIndex < 1) {\n return \"***\";\n }\n const localHead = local.slice(0, 2);\n const domainHead = domain.slice(0, 1);\n const tld = domain.slice(dotIndex);\n return `${localHead}***@${domainHead}***${tld}`;\n}\n\n/**\n * Replace every phone number and email address inside a free-form string with\n * its sanitized form. Used to scrub `Error.message` values before attaching\n * them to span status.\n */\nexport function sanitizeErrorMessage(input: string): string {\n return input\n .replace(EMAIL_PATTERN, (match) => sanitizeEmail(match))\n .replace(PHONE_PATTERN, (match) => sanitizePhone(match));\n}\n","export const PHOTON_OTEL_VERSION = \"0.1.0\";\n","import { context as otelContext } from \"@opentelemetry/api\";\nimport { type Logger, logs, SeverityNumber } from \"@opentelemetry/api-logs\";\nimport { PHOTON_OTEL_VERSION } from \"./version\";\n\nexport type LogAttrs = Record<string, string | number | boolean | undefined>;\n\n/**\n * Minimum severity that gets emitted (to both the OTLP record and the console).\n * `\"silent\"` suppresses everything, including errors.\n */\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"silent\";\n\nconst LEVEL_SEVERITY: Record<LogLevel, number> = {\n debug: SeverityNumber.DEBUG, // 5\n info: SeverityNumber.INFO, // 9\n warn: SeverityNumber.WARN, // 13\n error: SeverityNumber.ERROR, // 17\n silent: Number.POSITIVE_INFINITY,\n};\n\nlet levelOverride: LogLevel | undefined;\n\nfunction envLevel(): LogLevel | undefined {\n const raw = process.env.LOG_LEVEL?.toLowerCase();\n if (raw && raw in LEVEL_SEVERITY) {\n return raw as LogLevel;\n }\n return;\n}\n\nfunction defaultLevel(): LogLevel {\n return (process.env.DEPLOYMENT_ENV ?? \"development\") === \"development\"\n ? \"debug\"\n : \"info\";\n}\n\n/**\n * Resolve the active level fresh on each call so that `LOG_LEVEL` changes and\n * `setLogLevel()` both take effect immediately. Resolution order (env wins, to\n * match the rest of the package's config story):\n * 1. `LOG_LEVEL` env var\n * 2. `setLogLevel()` / `setupOtel({ logLevel })`\n * 3. environment-driven default (`debug` in development, `info` otherwise)\n */\nfunction resolveLevel(): LogLevel {\n return envLevel() ?? levelOverride ?? defaultLevel();\n}\n\n/**\n * Programmatically set the minimum log level. Takes effect immediately for\n * subsequent logs. `LOG_LEVEL` env var still wins if set.\n */\nexport function setLogLevel(level: LogLevel): void {\n levelOverride = level;\n}\n\n/** Current effective log level, after env / override / default resolution. */\nexport function getLogLevel(): LogLevel {\n return resolveLevel();\n}\n\nlet scopedLogger: Logger | undefined;\n\nfunction getLogger(): Logger {\n if (!scopedLogger) {\n scopedLogger = logs.getLogger(\"@photon-ai/otel\", PHOTON_OTEL_VERSION);\n }\n return scopedLogger;\n}\n\nfunction filterUndefined(\n attrs?: LogAttrs\n): Record<string, string | number | boolean> {\n if (!attrs) {\n return {};\n }\n const out: Record<string, string | number | boolean> = {};\n for (const [k, v] of Object.entries(attrs)) {\n if (v !== undefined) {\n out[k] = v;\n }\n }\n return out;\n}\n\nfunction consoleFor(\n severityNumber: SeverityNumber\n): (...args: unknown[]) => void {\n if (severityNumber >= SeverityNumber.ERROR) {\n return console.error;\n }\n if (severityNumber >= SeverityNumber.WARN) {\n return console.warn;\n }\n if (severityNumber >= SeverityNumber.INFO) {\n return console.info;\n }\n return console.debug;\n}\n\nfunction emit(\n severityNumber: SeverityNumber,\n severityText: string,\n module: string,\n message: string,\n attrs?: LogAttrs,\n error?: unknown\n): void {\n // Single gate: drop sub-threshold logs before they reach OTLP or the console.\n if (severityNumber < LEVEL_SEVERITY[resolveLevel()]) {\n return;\n }\n\n const userAttrs = filterUndefined(attrs);\n const attributes: Record<string, string | number | boolean> = {\n \"log.module\": module,\n ...userAttrs,\n };\n\n if (error instanceof Error) {\n attributes[\"exception.type\"] = error.name;\n attributes[\"exception.message\"] = error.message;\n if (error.stack) {\n attributes[\"exception.stacktrace\"] = error.stack;\n }\n } else if (error !== undefined) {\n // Don't silently drop non-Error throws (strings, plain objects, etc.).\n attributes[\"exception.type\"] = typeof error;\n attributes[\"exception.message\"] = String(error);\n }\n\n getLogger().emit({\n severityNumber,\n severityText,\n body: message,\n attributes,\n context: otelContext.active(),\n });\n\n // Console: `[module] LEVEL message { ...attrs }` plus the raw error so the\n // runtime renders the full stack and pretty-prints the attribute bag.\n const extras: unknown[] = [];\n if (Object.keys(userAttrs).length > 0) {\n extras.push(userAttrs);\n }\n if (error !== undefined) {\n extras.push(error);\n }\n consoleFor(severityNumber)(`[${module}]`, severityText, message, ...extras);\n}\n\nexport interface PhotonLogger {\n debug(message: string, attrs?: LogAttrs, error?: unknown): void;\n error(message: string, attrs?: LogAttrs, error?: unknown): void;\n info(message: string, attrs?: LogAttrs, error?: unknown): void;\n warn(message: string, attrs?: LogAttrs, error?: unknown): void;\n}\n\nexport function createLogger(module: string): PhotonLogger {\n return {\n debug: (message, attrs, error) =>\n emit(SeverityNumber.DEBUG, \"DEBUG\", module, message, attrs, error),\n info: (message, attrs, error) =>\n emit(SeverityNumber.INFO, \"INFO\", module, message, attrs, error),\n warn: (message, attrs, error) =>\n emit(SeverityNumber.WARN, \"WARN\", module, message, attrs, error),\n error: (message, attrs, error) =>\n emit(SeverityNumber.ERROR, \"ERROR\", module, message, attrs, error),\n };\n}\n","import { context, propagation, trace } from \"@opentelemetry/api\";\nimport { logs } from \"@opentelemetry/api-logs\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport {\n CompositePropagator,\n W3CBaggagePropagator,\n W3CTraceContextPropagator,\n} from \"@opentelemetry/core\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n BatchLogRecordProcessor,\n LoggerProvider,\n} from \"@opentelemetry/sdk-logs\";\nimport {\n BasicTracerProvider,\n BatchSpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport {\n type FetchInstrumentation,\n type InstrumentFetchOptions,\n instrumentFetch,\n} from \"./instrument-fetch\";\nimport { type LogLevel, setLogLevel } from \"./logger\";\n\nexport interface SetupOtelOptions {\n /**\n * Default OTLP/HTTP base endpoint (e.g. `https://otel.example.com`). The\n * `/v1/traces` and `/v1/logs` paths are appended automatically. Standard\n * `OTEL_EXPORTER_OTLP_*` env vars always take precedence.\n */\n endpoint?: string;\n /**\n * Default OTLP headers (e.g. `{ Authorization: \"Basic ...\" }`). Merged with\n * any headers parsed from `OTEL_EXPORTER_OTLP_HEADERS`; env values win on\n * conflicts.\n */\n headers?: Record<string, string>;\n /**\n * Auto-instrument outbound `globalThis.fetch` with CLIENT spans and W3C\n * trace-context propagation. On Bun this is the only fetch instrumentation\n * that works (diagnostics_channel-based instrumentations emit nothing on\n * Bun's native fetch); it works identically on Node.\n *\n * `true` enables with defaults; pass an object to filter URLs via `ignore`.\n * Defaults to enabled when a traces endpoint is configured. Pass `false` to\n * disable.\n */\n instrumentFetch?: boolean | InstrumentFetchOptions;\n /**\n * Minimum log level emitted by `createLogger()` (to both OTLP and console).\n * The `LOG_LEVEL` env var still takes precedence. Defaults to `debug` in\n * development and `info` otherwise.\n */\n logLevel?: LogLevel;\n /**\n * Extra resource attributes attached to every span/log alongside\n * `service.name` / `service.version`.\n */\n resourceAttributes?: Record<string, string | number | boolean>;\n serviceName: string;\n serviceVersion?: string;\n}\n\nexport interface OtelHandle {\n shutdown(): Promise<void>;\n}\n\nlet activeHandle: OtelHandle | undefined;\n\nconst TRAILING_SLASH = /\\/$/;\n\nfunction parseEnvHeaders(raw: string | undefined): Record<string, string> {\n if (!raw) {\n return {};\n }\n const out: Record<string, string> = {};\n for (const pair of raw.split(\",\")) {\n const eq = pair.indexOf(\"=\");\n if (eq <= 0) {\n continue;\n }\n const key = pair.slice(0, eq).trim();\n const value = pair.slice(eq + 1).trim();\n if (key) {\n out[key] = value;\n }\n }\n return out;\n}\n\nfunction resolveTracesEndpoint(base: string | undefined): string | undefined {\n const traces = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;\n if (traces) {\n return traces;\n }\n const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;\n return generic\n ? `${generic.replace(TRAILING_SLASH, \"\")}/v1/traces`\n : undefined;\n}\n\nfunction resolveLogsEndpoint(base: string | undefined): string | undefined {\n const logsEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;\n if (logsEndpoint) {\n return logsEndpoint;\n }\n const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;\n return generic ? `${generic.replace(TRAILING_SLASH, \"\")}/v1/logs` : undefined;\n}\n\n/**\n * Normalize a URL to an `origin + path` key (trailing slash stripped) for exact\n * self-trace matching. Returns `undefined` for unparseable URLs.\n */\nfunction otlpEndpointKey(url: string): string | undefined {\n try {\n const parsed = new URL(url);\n return `${parsed.origin}${parsed.pathname.replace(TRAILING_SLASH, \"\")}`;\n } catch {\n return;\n }\n}\n\nfunction otlpEndpointKeysOf(\n tracesEndpoint: string | undefined,\n logsEndpoint: string | undefined\n): string[] {\n const keys: string[] = [];\n for (const endpoint of [tracesEndpoint, logsEndpoint]) {\n if (!endpoint) {\n continue;\n }\n const key = otlpEndpointKey(endpoint);\n if (key) {\n keys.push(key);\n }\n }\n return keys;\n}\n\n/**\n * Patch `globalThis.fetch` unless disabled. Defaults to on when a traces\n * pipeline is configured. Always excludes our own OTLP endpoints so the\n * exporter's traffic is never self-traced (matters on Node, where the OTLP\n * exporter can use fetch).\n */\nfunction startFetchInstrumentation(\n option: boolean | InstrumentFetchOptions | undefined,\n hasTraces: boolean,\n tracesEndpoint: string | undefined,\n logsEndpoint: string | undefined\n): FetchInstrumentation | undefined {\n const want = option ?? hasTraces;\n if (!want) {\n return;\n }\n const userOptions = typeof option === \"object\" ? option : undefined;\n const otlpEndpointKeys = otlpEndpointKeysOf(tracesEndpoint, logsEndpoint);\n return instrumentFetch({\n ignore: (url) => {\n const key = otlpEndpointKey(url);\n const isOtlpEndpoint =\n key !== undefined && otlpEndpointKeys.includes(key);\n return isOtlpEndpoint || (userOptions?.ignore?.(url) ?? false);\n },\n });\n}\n\n/**\n * Boot an OTLP/HTTP-based OpenTelemetry pipeline (traces + logs).\n *\n * Idempotent: calling twice in the same process is a no-op on the second\n * call, so libraries can safely invoke this without clobbering an app-level\n * OTel setup that ran earlier.\n *\n * Standard `OTEL_EXPORTER_OTLP_*` env vars override the `endpoint` and\n * `headers` arguments — this matches the OpenTelemetry SDK config spec.\n */\nexport function setupOtel(options: SetupOtelOptions): OtelHandle {\n if (activeHandle) {\n return activeHandle;\n }\n\n if (options.logLevel) {\n setLogLevel(options.logLevel);\n }\n\n const tracesEndpoint = resolveTracesEndpoint(options.endpoint);\n const logsEndpoint = resolveLogsEndpoint(options.endpoint);\n const mergedHeaders = {\n ...options.headers,\n ...parseEnvHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS),\n };\n const hasHeaders = Object.keys(mergedHeaders).length > 0;\n\n const resource = resourceFromAttributes({\n \"service.name\": options.serviceName,\n ...(options.serviceVersion\n ? { \"service.version\": options.serviceVersion }\n : {}),\n \"deployment.environment\": process.env.DEPLOYMENT_ENV ?? \"development\",\n ...options.resourceAttributes,\n });\n\n context.setGlobalContextManager(new AsyncLocalStorageContextManager());\n propagation.setGlobalPropagator(\n new CompositePropagator({\n propagators: [\n new W3CTraceContextPropagator(),\n new W3CBaggagePropagator(),\n ],\n })\n );\n\n const traceProcessors = tracesEndpoint\n ? [\n new BatchSpanProcessor(\n new OTLPTraceExporter({\n url: tracesEndpoint,\n headers: hasHeaders ? mergedHeaders : undefined,\n })\n ),\n ]\n : [];\n\n const tracerProvider = new BasicTracerProvider({\n resource,\n spanProcessors: traceProcessors,\n });\n trace.setGlobalTracerProvider(tracerProvider);\n\n const fetchInstrumentation = startFetchInstrumentation(\n options.instrumentFetch,\n traceProcessors.length > 0,\n tracesEndpoint,\n logsEndpoint\n );\n\n const logProcessors = logsEndpoint\n ? [\n new BatchLogRecordProcessor(\n new OTLPLogExporter({\n url: logsEndpoint,\n headers: hasHeaders ? mergedHeaders : undefined,\n })\n ),\n ]\n : [];\n\n const loggerProvider = new LoggerProvider({\n resource,\n processors: logProcessors,\n });\n logs.setGlobalLoggerProvider(loggerProvider);\n\n const handle: OtelHandle = {\n async shutdown() {\n fetchInstrumentation?.unpatch();\n await Promise.allSettled([\n tracerProvider.shutdown(),\n loggerProvider.shutdown(),\n ]);\n activeHandle = undefined;\n },\n };\n\n activeHandle = handle;\n return handle;\n}\n\n/**\n * Read-only accessor for tests / debug paths that need to know whether\n * `setupOtel` has already run in this process.\n */\nexport function isOtelActive(): boolean {\n return activeHandle !== undefined;\n}\n","import {\n type Attributes,\n SpanStatusCode,\n type Tracer,\n trace,\n} from \"@opentelemetry/api\";\nimport type { LogAttrs } from \"./logger\";\nimport { sanitizeErrorMessage } from \"./sanitize\";\nimport { PHOTON_OTEL_VERSION } from \"./version\";\n\nlet scopedTracer: Tracer | undefined;\n\nfunction getTracer(): Tracer {\n if (!scopedTracer) {\n scopedTracer = trace.getTracer(\"@photon-ai/otel\", PHOTON_OTEL_VERSION);\n }\n return scopedTracer;\n}\n\nfunction toAttributes(attrs: LogAttrs): Attributes {\n const out: Attributes = {};\n for (const [k, v] of Object.entries(attrs)) {\n if (v !== undefined) {\n out[k] = v;\n }\n }\n return out;\n}\n\nexport function withSpan<T>(name: string, fn: () => Promise<T> | T): Promise<T>;\nexport function withSpan<T>(\n name: string,\n attrs: LogAttrs,\n fn: () => Promise<T> | T\n): Promise<T>;\nexport function withSpan<T>(\n name: string,\n attrsOrFn: LogAttrs | (() => Promise<T> | T),\n maybeFn?: () => Promise<T> | T\n): Promise<T> {\n const fn = typeof attrsOrFn === \"function\" ? attrsOrFn : maybeFn;\n if (!fn) {\n throw new Error(\"withSpan: function argument is required\");\n }\n const attrs = typeof attrsOrFn === \"function\" ? undefined : attrsOrFn;\n\n return getTracer().startActiveSpan(name, async (span) => {\n if (attrs) {\n span.setAttributes(toAttributes(attrs));\n }\n try {\n const result = await fn();\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (err) {\n span.recordException(err as Error);\n const errorObj = err instanceof Error ? err : undefined;\n span.setAttribute(\"error.type\", errorObj?.constructor.name ?? typeof err);\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: errorObj\n ? sanitizeErrorMessage(errorObj.message)\n : sanitizeErrorMessage(String(err)),\n });\n throw err;\n } finally {\n span.end();\n }\n });\n}\n"],"mappings":";AAAA;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;;;ACfP,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AASf,SAAS,cAAc,OAAuB;AACnD,QAAM,UAAU,MAAM,WAAW,GAAG;AACpC,QAAM,SAAS,MAAM,QAAQ,OAAO,EAAE;AACtC,MAAI,OAAO,SAAS,GAAG;AACrB,WAAO,UAAU,UAAU;AAAA,EAC7B;AACA,QAAM,OAAO,OAAO,MAAM,GAAG,CAAC;AAC9B,QAAM,OAAO,OAAO,MAAM,EAAE;AAC5B,QAAM,eAAe,OAAO,SAAS,KAAK,SAAS,KAAK;AACxD,SAAO,GAAG,UAAU,MAAM,EAAE,GAAG,IAAI,GAAG,IAAI,OAAO,YAAY,CAAC,GAAG,IAAI;AACvE;AAOO,SAAS,cAAc,OAAuB;AACnD,QAAM,UAAU,MAAM,YAAY,GAAG;AACrC,MAAI,UAAU,GAAG;AACf,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,MAAM,MAAM,GAAG,OAAO;AACpC,QAAM,SAAS,MAAM,MAAM,UAAU,CAAC;AACtC,QAAM,WAAW,OAAO,YAAY,GAAG;AACvC,MAAI,WAAW,GAAG;AAChB,WAAO;AAAA,EACT;AACA,QAAM,YAAY,MAAM,MAAM,GAAG,CAAC;AAClC,QAAM,aAAa,OAAO,MAAM,GAAG,CAAC;AACpC,QAAM,MAAM,OAAO,MAAM,QAAQ;AACjC,SAAO,GAAG,SAAS,OAAO,UAAU,MAAM,GAAG;AAC/C;AAOO,SAAS,qBAAqB,OAAuB;AAC1D,SAAO,MACJ,QAAQ,eAAe,CAAC,UAAU,cAAc,KAAK,CAAC,EACtD,QAAQ,eAAe,CAAC,UAAU,cAAc,KAAK,CAAC;AAC3D;;;ACtDO,IAAM,sBAAsB;;;AF4CnC,IAAM,eAAe,uBAAO,IAAI,gCAAgC;AAEhE,IAAM,wBAAwB;AAC9B,IAAM,gBAAwC,EAAE,UAAU,KAAK,SAAS,GAAG;AAE3E,IAAI;AAEJ,SAAS,YAAoB;AAC3B,MAAI,CAAC,cAAc;AACjB,mBAAe,MAAM,UAAU,mBAAmB,mBAAmB;AAAA,EACvE;AACA,SAAO;AACT;AAEA,SAAS,eAAe,IAAmB;AAGzC,aAAW,QAAQ;AACrB;AAEA,SAAS,iBAAiB,IAAkC;AAC1D,SAAQ,GAAsD,YAAY;AAC5E;AAEA,SAAS,iBAAiB,IAAa,UAAyB;AAC9D,EAAC,GAA0C,YAAY,IAAI;AAC7D;AAGA,SAAS,cAAc,MAAe,IAAmB;AACvD,aAAW,OAAO,OAAO,oBAAoB,IAAI,GAAG;AAClD,QAAI,OAAO,IAAI;AACb;AAAA,IACF;AACA,UAAM,aAAa,OAAO,yBAAyB,MAAM,GAAG;AAC5D,QAAI,YAAY;AACd,aAAO,eAAe,IAAI,KAAK,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AAEA,SAAS,mBACP,OACA,MACiC;AACjC,MAAI,iBAAiB,SAAS;AAC5B,WAAO,EAAE,QAAQ,MAAM,QAAQ,KAAK,MAAM,IAAI;AAAA,EAChD;AACA,QAAM,MAAM,OAAO,UAAU,WAAW,QAAQ,MAAM,SAAS;AAC/D,SAAO,EAAE,QAAQ,MAAM,UAAU,OAAO,IAAI;AAC9C;AAEA,SAAS,YAAY,QAAiC;AACpD,MAAI,OAAO,MAAM;AACf,WAAO,OAAO,OAAO,IAAI;AAAA,EAC3B;AACA,SAAO,cAAc,OAAO,QAAQ;AACtC;AAEA,SAAS,aAAa,OAA+B;AACnD,QAAM,MAAkB,CAAC;AACzB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,UAAU,QAAW;AACvB,UAAI,GAAG,IAAI;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,gBAAgB,QAAgB,KAAyB;AAChE,QAAM,QAAoB;AAAA,IACxB,CAAC,wBAAwB,GAAG;AAAA,IAC5B,CAAC,aAAa,GAAG;AAAA,EACnB;AACA,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAM,mBAAmB,IAAI,OAAO,YAAY;AAChD,UAAM,gBAAgB,IAAI,YAAY,MAAM;AAAA,EAC9C,QAAQ;AAAA,EAER;AACA,SAAO,aAAa,KAAK;AAC3B;AAGA,SAAS,uBAAuB,OAAmB,MAA0B;AAC3E,QAAM,UAAU,IAAI;AAAA,IAClB,iBAAiB,UAAU,MAAM,UAAU;AAAA,EAC7C;AACA,MAAI,MAAM,SAAS;AACjB,eAAW,CAAC,KAAK,KAAK,KAAK,IAAI,QAAQ,KAAK,OAAO,EAAE,QAAQ,GAAG;AAC9D,cAAQ,IAAI,KAAK,KAAK;AAAA,IACxB;AAAA,EACF;AACA,cAAY,OAAO,QAAQ,OAAO,GAAG,SAAS;AAAA,IAC5C,KAAK,CAAC,SAAS,KAAK,UAAU;AAC5B,cAAQ,IAAI,KAAK,KAAK;AAAA,IACxB;AAAA,EACF,CAAC;AACD,SAAO;AACT;AAEA,SAAS,aACP,UACA,OACA,MACA,SACmB;AACnB,MAAI,iBAAiB,SAAS;AAI5B,QAAI,MAAM,UAAU;AAClB,iBAAW,CAAC,KAAK,KAAK,KAAK,QAAQ,QAAQ,GAAG;AAC5C,cAAM,QAAQ,IAAI,KAAK,KAAK;AAAA,MAC9B;AACA,aAAO,SAAS,OAAO,IAAI;AAAA,IAC7B;AACA,WAAO,SAAS,IAAI,QAAQ,OAAO,EAAE,GAAG,MAAM,QAAQ,CAAC,CAAC;AAAA,EAC1D;AACA,SAAO,SAAS,OAAO,EAAE,GAAG,MAAM,QAAQ,CAAC;AAC7C;AAEA,SAAS,kBACP,UACA,SACS;AACT,SAAO,CAAC,OAAO,SAAS;AACtB,UAAM,EAAE,QAAQ,IAAI,IAAI,mBAAmB,OAAO,IAAI;AACtD,QAAI,SAAS,SAAS,GAAG,GAAG;AAC1B,aAAO,SAAS,OAAO,IAAI;AAAA,IAC7B;AACA,UAAM,OAAO,OAAO,YAAY;AAChC,WAAO,UAAU,EAAE;AAAA,MACjB;AAAA,MACA,EAAE,MAAM,SAAS,OAAO;AAAA,MACxB,OAAO,SAAS;AACd,aAAK,cAAc,gBAAgB,MAAM,GAAG,CAAC;AAC7C,YAAI;AACF,gBAAM,UAAU,uBAAuB,OAAO,IAAI;AAClD,gBAAM,WAAW,MAAM,aAAa,UAAU,OAAO,MAAM,OAAO;AAClE,eAAK,aAAa,gCAAgC,SAAS,MAAM;AACjE,eAAK,UAAU;AAAA,YACb,MACE,SAAS,UAAU,wBACf,eAAe,QACf,eAAe;AAAA,UACvB,CAAC;AACD,iBAAO;AAAA,QACT,SAAS,KAAK;AACZ,eAAK,gBAAgB,GAAY;AACjC,gBAAM,WAAW,eAAe,QAAQ,MAAM;AAC9C,eAAK;AAAA,YACH;AAAA,YACA,UAAU,YAAY,QAAQ,OAAO;AAAA,UACvC;AACA,eAAK,UAAU;AAAA,YACb,MAAM,eAAe;AAAA,YACrB,SAAS,WACL,qBAAqB,SAAS,OAAO,IACrC,qBAAqB,OAAO,GAAG,CAAC;AAAA,UACtC,CAAC;AACD,gBAAM;AAAA,QACR,UAAE;AACA,eAAK,IAAI;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAeO,SAAS,gBACd,SACsB;AACtB,QAAM,UAAmB,WAAW;AACpC,QAAM,mBAAmB,iBAAiB,OAAO;AACjD,MAAI,kBAAkB;AACpB,WAAO;AAAA,MACL,UAAU;AACR,YAAI,WAAW,UAAU,SAAS;AAChC,yBAAe,gBAAgB;AAAA,QACjC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW;AACjB,QAAM,UAAU,kBAAkB,UAAU,OAAO;AACnD,gBAAc,UAAU,OAAO;AAC/B,mBAAiB,SAAS,QAAQ;AAClC,iBAAe,OAAO;AAEtB,SAAO;AAAA,IACL,UAAU;AACR,UAAI,WAAW,UAAU,SAAS;AAChC,uBAAe,QAAQ;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AACF;;;AGhQA,SAAS,WAAW,mBAAmB;AACvC,SAAsB,MAAM,sBAAsB;AAWlD,IAAM,iBAA2C;AAAA,EAC/C,OAAO,eAAe;AAAA;AAAA,EACtB,MAAM,eAAe;AAAA;AAAA,EACrB,MAAM,eAAe;AAAA;AAAA,EACrB,OAAO,eAAe;AAAA;AAAA,EACtB,QAAQ,OAAO;AACjB;AAEA,IAAI;AAEJ,SAAS,WAAiC;AACxC,QAAM,MAAM,QAAQ,IAAI,WAAW,YAAY;AAC/C,MAAI,OAAO,OAAO,gBAAgB;AAChC,WAAO;AAAA,EACT;AACA;AACF;AAEA,SAAS,eAAyB;AAChC,UAAQ,QAAQ,IAAI,kBAAkB,mBAAmB,gBACrD,UACA;AACN;AAUA,SAAS,eAAyB;AAChC,SAAO,SAAS,KAAK,iBAAiB,aAAa;AACrD;AAMO,SAAS,YAAY,OAAuB;AACjD,kBAAgB;AAClB;AAGO,SAAS,cAAwB;AACtC,SAAO,aAAa;AACtB;AAEA,IAAI;AAEJ,SAAS,YAAoB;AAC3B,MAAI,CAAC,cAAc;AACjB,mBAAe,KAAK,UAAU,mBAAmB,mBAAmB;AAAA,EACtE;AACA,SAAO;AACT;AAEA,SAAS,gBACP,OAC2C;AAC3C,MAAI,CAAC,OAAO;AACV,WAAO,CAAC;AAAA,EACV;AACA,QAAM,MAAiD,CAAC;AACxD,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC1C,QAAI,MAAM,QAAW;AACnB,UAAI,CAAC,IAAI;AAAA,IACX;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,WACP,gBAC8B;AAC9B,MAAI,kBAAkB,eAAe,OAAO;AAC1C,WAAO,QAAQ;AAAA,EACjB;AACA,MAAI,kBAAkB,eAAe,MAAM;AACzC,WAAO,QAAQ;AAAA,EACjB;AACA,MAAI,kBAAkB,eAAe,MAAM;AACzC,WAAO,QAAQ;AAAA,EACjB;AACA,SAAO,QAAQ;AACjB;AAEA,SAAS,KACP,gBACA,cACA,QACA,SACA,OACA,OACM;AAEN,MAAI,iBAAiB,eAAe,aAAa,CAAC,GAAG;AACnD;AAAA,EACF;AAEA,QAAM,YAAY,gBAAgB,KAAK;AACvC,QAAM,aAAwD;AAAA,IAC5D,cAAc;AAAA,IACd,GAAG;AAAA,EACL;AAEA,MAAI,iBAAiB,OAAO;AAC1B,eAAW,gBAAgB,IAAI,MAAM;AACrC,eAAW,mBAAmB,IAAI,MAAM;AACxC,QAAI,MAAM,OAAO;AACf,iBAAW,sBAAsB,IAAI,MAAM;AAAA,IAC7C;AAAA,EACF,WAAW,UAAU,QAAW;AAE9B,eAAW,gBAAgB,IAAI,OAAO;AACtC,eAAW,mBAAmB,IAAI,OAAO,KAAK;AAAA,EAChD;AAEA,YAAU,EAAE,KAAK;AAAA,IACf;AAAA,IACA;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA,SAAS,YAAY,OAAO;AAAA,EAC9B,CAAC;AAID,QAAM,SAAoB,CAAC;AAC3B,MAAI,OAAO,KAAK,SAAS,EAAE,SAAS,GAAG;AACrC,WAAO,KAAK,SAAS;AAAA,EACvB;AACA,MAAI,UAAU,QAAW;AACvB,WAAO,KAAK,KAAK;AAAA,EACnB;AACA,aAAW,cAAc,EAAE,IAAI,MAAM,KAAK,cAAc,SAAS,GAAG,MAAM;AAC5E;AASO,SAAS,aAAa,QAA8B;AACzD,SAAO;AAAA,IACL,OAAO,CAAC,SAAS,OAAO,UACtB,KAAK,eAAe,OAAO,SAAS,QAAQ,SAAS,OAAO,KAAK;AAAA,IACnE,MAAM,CAAC,SAAS,OAAO,UACrB,KAAK,eAAe,MAAM,QAAQ,QAAQ,SAAS,OAAO,KAAK;AAAA,IACjE,MAAM,CAAC,SAAS,OAAO,UACrB,KAAK,eAAe,MAAM,QAAQ,QAAQ,SAAS,OAAO,KAAK;AAAA,IACjE,OAAO,CAAC,SAAS,OAAO,UACtB,KAAK,eAAe,OAAO,SAAS,QAAQ,SAAS,OAAO,KAAK;AAAA,EACrE;AACF;;;ACzKA,SAAS,WAAAA,UAAS,eAAAC,cAAa,SAAAC,cAAa;AAC5C,SAAS,QAAAC,aAAY;AACrB,SAAS,uCAAuC;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB;AAChC,SAAS,yBAAyB;AAClC,SAAS,8BAA8B;AACvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAmDP,IAAI;AAEJ,IAAM,iBAAiB;AAEvB,SAAS,gBAAgB,KAAiD;AACxE,MAAI,CAAC,KAAK;AACR,WAAO,CAAC;AAAA,EACV;AACA,QAAM,MAA8B,CAAC;AACrC,aAAW,QAAQ,IAAI,MAAM,GAAG,GAAG;AACjC,UAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,QAAI,MAAM,GAAG;AACX;AAAA,IACF;AACA,UAAM,MAAM,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK;AACnC,UAAM,QAAQ,KAAK,MAAM,KAAK,CAAC,EAAE,KAAK;AACtC,QAAI,KAAK;AACP,UAAI,GAAG,IAAI;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,sBAAsB,MAA8C;AAC3E,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,QAAQ;AACV,WAAO;AAAA,EACT;AACA,QAAM,UAAU,QAAQ,IAAI,+BAA+B;AAC3D,SAAO,UACH,GAAG,QAAQ,QAAQ,gBAAgB,EAAE,CAAC,eACtC;AACN;AAEA,SAAS,oBAAoB,MAA8C;AACzE,QAAM,eAAe,QAAQ,IAAI;AACjC,MAAI,cAAc;AAChB,WAAO;AAAA,EACT;AACA,QAAM,UAAU,QAAQ,IAAI,+BAA+B;AAC3D,SAAO,UAAU,GAAG,QAAQ,QAAQ,gBAAgB,EAAE,CAAC,aAAa;AACtE;AAMA,SAAS,gBAAgB,KAAiC;AACxD,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,WAAO,GAAG,OAAO,MAAM,GAAG,OAAO,SAAS,QAAQ,gBAAgB,EAAE,CAAC;AAAA,EACvE,QAAQ;AACN;AAAA,EACF;AACF;AAEA,SAAS,mBACP,gBACA,cACU;AACV,QAAM,OAAiB,CAAC;AACxB,aAAW,YAAY,CAAC,gBAAgB,YAAY,GAAG;AACrD,QAAI,CAAC,UAAU;AACb;AAAA,IACF;AACA,UAAM,MAAM,gBAAgB,QAAQ;AACpC,QAAI,KAAK;AACP,WAAK,KAAK,GAAG;AAAA,IACf;AAAA,EACF;AACA,SAAO;AACT;AAQA,SAAS,0BACP,QACA,WACA,gBACA,cACkC;AAClC,QAAM,OAAO,UAAU;AACvB,MAAI,CAAC,MAAM;AACT;AAAA,EACF;AACA,QAAM,cAAc,OAAO,WAAW,WAAW,SAAS;AAC1D,QAAM,mBAAmB,mBAAmB,gBAAgB,YAAY;AACxE,SAAO,gBAAgB;AAAA,IACrB,QAAQ,CAAC,QAAQ;AACf,YAAM,MAAM,gBAAgB,GAAG;AAC/B,YAAM,iBACJ,QAAQ,UAAa,iBAAiB,SAAS,GAAG;AACpD,aAAO,mBAAmB,aAAa,SAAS,GAAG,KAAK;AAAA,IAC1D;AAAA,EACF,CAAC;AACH;AAYO,SAAS,UAAU,SAAuC;AAC/D,MAAI,cAAc;AAChB,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,UAAU;AACpB,gBAAY,QAAQ,QAAQ;AAAA,EAC9B;AAEA,QAAM,iBAAiB,sBAAsB,QAAQ,QAAQ;AAC7D,QAAM,eAAe,oBAAoB,QAAQ,QAAQ;AACzD,QAAM,gBAAgB;AAAA,IACpB,GAAG,QAAQ;AAAA,IACX,GAAG,gBAAgB,QAAQ,IAAI,0BAA0B;AAAA,EAC3D;AACA,QAAM,aAAa,OAAO,KAAK,aAAa,EAAE,SAAS;AAEvD,QAAM,WAAW,uBAAuB;AAAA,IACtC,gBAAgB,QAAQ;AAAA,IACxB,GAAI,QAAQ,iBACR,EAAE,mBAAmB,QAAQ,eAAe,IAC5C,CAAC;AAAA,IACL,0BAA0B,QAAQ,IAAI,kBAAkB;AAAA,IACxD,GAAG,QAAQ;AAAA,EACb,CAAC;AAED,EAAAC,SAAQ,wBAAwB,IAAI,gCAAgC,CAAC;AACrE,EAAAC,aAAY;AAAA,IACV,IAAI,oBAAoB;AAAA,MACtB,aAAa;AAAA,QACX,IAAI,0BAA0B;AAAA,QAC9B,IAAI,qBAAqB;AAAA,MAC3B;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,kBAAkB,iBACpB;AAAA,IACE,IAAI;AAAA,MACF,IAAI,kBAAkB;AAAA,QACpB,KAAK;AAAA,QACL,SAAS,aAAa,gBAAgB;AAAA,MACxC,CAAC;AAAA,IACH;AAAA,EACF,IACA,CAAC;AAEL,QAAM,iBAAiB,IAAI,oBAAoB;AAAA,IAC7C;AAAA,IACA,gBAAgB;AAAA,EAClB,CAAC;AACD,EAAAC,OAAM,wBAAwB,cAAc;AAE5C,QAAM,uBAAuB;AAAA,IAC3B,QAAQ;AAAA,IACR,gBAAgB,SAAS;AAAA,IACzB;AAAA,IACA;AAAA,EACF;AAEA,QAAM,gBAAgB,eAClB;AAAA,IACE,IAAI;AAAA,MACF,IAAI,gBAAgB;AAAA,QAClB,KAAK;AAAA,QACL,SAAS,aAAa,gBAAgB;AAAA,MACxC,CAAC;AAAA,IACH;AAAA,EACF,IACA,CAAC;AAEL,QAAM,iBAAiB,IAAI,eAAe;AAAA,IACxC;AAAA,IACA,YAAY;AAAA,EACd,CAAC;AACD,EAAAC,MAAK,wBAAwB,cAAc;AAE3C,QAAM,SAAqB;AAAA,IACzB,MAAM,WAAW;AACf,4BAAsB,QAAQ;AAC9B,YAAM,QAAQ,WAAW;AAAA,QACvB,eAAe,SAAS;AAAA,QACxB,eAAe,SAAS;AAAA,MAC1B,CAAC;AACD,qBAAe;AAAA,IACjB;AAAA,EACF;AAEA,iBAAe;AACf,SAAO;AACT;AAMO,SAAS,eAAwB;AACtC,SAAO,iBAAiB;AAC1B;;;ACtRA;AAAA,EAEE,kBAAAC;AAAA,EAEA,SAAAC;AAAA,OACK;AAKP,IAAIC;AAEJ,SAASC,aAAoB;AAC3B,MAAI,CAACD,eAAc;AACjB,IAAAA,gBAAeE,OAAM,UAAU,mBAAmB,mBAAmB;AAAA,EACvE;AACA,SAAOF;AACT;AAEA,SAASG,cAAa,OAA6B;AACjD,QAAM,MAAkB,CAAC;AACzB,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK,GAAG;AAC1C,QAAI,MAAM,QAAW;AACnB,UAAI,CAAC,IAAI;AAAA,IACX;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,SACd,MACA,WACA,SACY;AACZ,QAAM,KAAK,OAAO,cAAc,aAAa,YAAY;AACzD,MAAI,CAAC,IAAI;AACP,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AACA,QAAM,QAAQ,OAAO,cAAc,aAAa,SAAY;AAE5D,SAAOF,WAAU,EAAE,gBAAgB,MAAM,OAAO,SAAS;AACvD,QAAI,OAAO;AACT,WAAK,cAAcE,cAAa,KAAK,CAAC;AAAA,IACxC;AACA,QAAI;AACF,YAAM,SAAS,MAAM,GAAG;AACxB,WAAK,UAAU,EAAE,MAAMC,gBAAe,GAAG,CAAC;AAC1C,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,WAAK,gBAAgB,GAAY;AACjC,YAAM,WAAW,eAAe,QAAQ,MAAM;AAC9C,WAAK,aAAa,cAAc,UAAU,YAAY,QAAQ,OAAO,GAAG;AACxE,WAAK,UAAU;AAAA,QACb,MAAMA,gBAAe;AAAA,QACrB,SAAS,WACL,qBAAqB,SAAS,OAAO,IACrC,qBAAqB,OAAO,GAAG,CAAC;AAAA,MACtC,CAAC;AACD,YAAM;AAAA,IACR,UAAE;AACA,WAAK,IAAI;AAAA,IACX;AAAA,EACF,CAAC;AACH;","names":["context","propagation","trace","logs","context","propagation","trace","logs","SpanStatusCode","trace","scopedTracer","getTracer","trace","toAttributes","SpanStatusCode"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@photon-ai/otel",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "DX-focused OpenTelemetry wrapper for Bun and Node.js: one-call setup, structured logging, span helper, and built-in PII scrubbing.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,11 +20,13 @@
20
20
  "@opentelemetry/api": "^1.9.1",
21
21
  "@opentelemetry/api-logs": "^0.218.0",
22
22
  "@opentelemetry/context-async-hooks": "^2.7.1",
23
+ "@opentelemetry/core": "^2.7.1",
23
24
  "@opentelemetry/exporter-logs-otlp-http": "^0.218.0",
24
25
  "@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
25
26
  "@opentelemetry/resources": "^2.7.1",
26
27
  "@opentelemetry/sdk-logs": "^0.218.0",
27
- "@opentelemetry/sdk-trace-base": "^2.7.1"
28
+ "@opentelemetry/sdk-trace-base": "^2.7.1",
29
+ "@opentelemetry/semantic-conventions": "^1.41.1"
28
30
  },
29
31
  "peerDependencies": {
30
32
  "typescript": "^5 || ^6.0.0"