@photon-ai/otel 1.1.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -19
- package/dist/index.d.ts +111 -58
- package/dist/index.js +516 -497
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,555 +1,574 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "@opentelemetry/
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
*/
|
|
21
22
|
function sanitizePhone(input) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
*/
|
|
32
36
|
function sanitizeEmail(input) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return `${localHead}***@${domainHead}***${tld}`;
|
|
47
|
-
}
|
|
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
|
+
*/
|
|
48
50
|
function sanitizeErrorMessage(input) {
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
69
|
+
};
|
|
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;
|
|
65
74
|
}
|
|
66
75
|
function setGlobalFetch(fn) {
|
|
67
|
-
|
|
76
|
+
globalThis.fetch = fn;
|
|
68
77
|
}
|
|
69
78
|
function getPatchOriginal(fn) {
|
|
70
|
-
|
|
79
|
+
return fn[PATCH_MARKER];
|
|
71
80
|
}
|
|
72
81
|
function setPatchOriginal(fn, original) {
|
|
73
|
-
|
|
82
|
+
fn[PATCH_MARKER] = original;
|
|
74
83
|
}
|
|
84
|
+
/** Copy extra own properties (e.g. Bun's `fetch.preconnect`) onto the wrapper. */
|
|
75
85
|
function preserveProps(from, to) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (descriptor) {
|
|
82
|
-
Object.defineProperty(to, key, descriptor);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
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
|
+
}
|
|
85
91
|
}
|
|
86
92
|
function resolveRequestMeta(input, init) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
};
|
|
92
102
|
}
|
|
93
103
|
function resolvePort(parsed) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
return DEFAULT_PORTS[parsed.protocol];
|
|
104
|
+
if (parsed.port) return Number(parsed.port);
|
|
105
|
+
return DEFAULT_PORTS[parsed.protocol];
|
|
98
106
|
}
|
|
99
|
-
function toAttributes(attrs) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
out[key] = value;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
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;
|
|
107
111
|
}
|
|
108
112
|
function fetchAttributes(method, url) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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. */
|
|
121
125
|
function buildPropagatedHeaders(input, init) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
propagation.inject(context.active(), headers, {
|
|
131
|
-
set: (carrier, key, value) => {
|
|
132
|
-
carrier.set(key, value);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
return headers;
|
|
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;
|
|
136
132
|
}
|
|
137
133
|
function callOriginal(original, input, init, headers) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
148
|
}
|
|
149
149
|
function buildWrappedFetch(original, options) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
*/
|
|
188
217
|
function instrumentFetch(options) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// src/logger.ts
|
|
215
|
-
import { context as otelContext } from "@opentelemetry/api";
|
|
216
|
-
import { logs, SeverityNumber } from "@opentelemetry/api-logs";
|
|
217
|
-
var LEVEL_SEVERITY = {
|
|
218
|
-
debug: SeverityNumber.DEBUG,
|
|
219
|
-
// 5
|
|
220
|
-
info: SeverityNumber.INFO,
|
|
221
|
-
// 9
|
|
222
|
-
warn: SeverityNumber.WARN,
|
|
223
|
-
// 13
|
|
224
|
-
error: SeverityNumber.ERROR,
|
|
225
|
-
// 17
|
|
226
|
-
silent: Number.POSITIVE_INFINITY
|
|
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
|
|
227
238
|
};
|
|
228
|
-
|
|
239
|
+
let levelOverride;
|
|
229
240
|
function envLevel() {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return raw;
|
|
233
|
-
}
|
|
234
|
-
return;
|
|
241
|
+
const raw = process.env.LOG_LEVEL?.toLowerCase();
|
|
242
|
+
if (raw && raw in LEVEL_SEVERITY) return raw;
|
|
235
243
|
}
|
|
236
244
|
function defaultLevel() {
|
|
237
|
-
|
|
238
|
-
}
|
|
245
|
+
return (process.env.DEPLOYMENT_ENV ?? "development") === "development" ? "debug" : "info";
|
|
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
|
+
*/
|
|
239
255
|
function resolveLevel() {
|
|
240
|
-
|
|
256
|
+
return envLevel() ?? levelOverride ?? defaultLevel();
|
|
241
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
|
+
*/
|
|
242
262
|
function setLogLevel(level) {
|
|
243
|
-
|
|
263
|
+
levelOverride = level;
|
|
244
264
|
}
|
|
265
|
+
/** Current effective log level, after env / override / default resolution. */
|
|
245
266
|
function getLogLevel() {
|
|
246
|
-
|
|
267
|
+
return resolveLevel();
|
|
247
268
|
}
|
|
248
|
-
|
|
269
|
+
let scopedLogger;
|
|
249
270
|
function getLogger() {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
return scopedLogger;
|
|
271
|
+
if (!scopedLogger) scopedLogger = logs.getLogger("@photon-ai/otel", PHOTON_OTEL_VERSION);
|
|
272
|
+
return scopedLogger;
|
|
254
273
|
}
|
|
255
274
|
function filterUndefined(attrs) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
for (const [k, v] of Object.entries(attrs)) {
|
|
261
|
-
if (v !== void 0) {
|
|
262
|
-
out[k] = v;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
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;
|
|
266
279
|
}
|
|
267
280
|
function consoleFor(severityNumber) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return console.warn;
|
|
273
|
-
}
|
|
274
|
-
if (severityNumber >= SeverityNumber.INFO) {
|
|
275
|
-
return console.info;
|
|
276
|
-
}
|
|
277
|
-
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;
|
|
278
285
|
}
|
|
279
286
|
function emit(severityNumber, severityText, module, message, attrs, error) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const extras = [];
|
|
306
|
-
if (Object.keys(userAttrs).length > 0) {
|
|
307
|
-
extras.push(userAttrs);
|
|
308
|
-
}
|
|
309
|
-
if (error !== void 0) {
|
|
310
|
-
extras.push(error);
|
|
311
|
-
}
|
|
312
|
-
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);
|
|
313
312
|
}
|
|
314
313
|
function createLogger(module) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
};
|
|
320
|
+
}
|
|
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
|
+
}
|
|
335
|
+
}
|
|
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);
|
|
349
|
+
}
|
|
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
|
+
} };
|
|
380
|
+
}
|
|
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 = /\/$/;
|
|
345
401
|
function parseEnvHeaders(raw) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const value = pair.slice(eq + 1).trim();
|
|
357
|
-
if (key) {
|
|
358
|
-
out[key] = value;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
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;
|
|
362
412
|
}
|
|
363
413
|
function resolveTracesEndpoint(base) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const generic = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? base;
|
|
369
|
-
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;
|
|
370
418
|
}
|
|
371
419
|
function resolveLogsEndpoint(base) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
*/
|
|
379
429
|
function otlpEndpointKey(url) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
430
|
+
try {
|
|
431
|
+
const parsed = new URL(url);
|
|
432
|
+
return `${parsed.origin}${parsed.pathname.replace(TRAILING_SLASH, "")}`;
|
|
433
|
+
} catch {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
386
436
|
}
|
|
387
437
|
function otlpEndpointKeysOf(tracesEndpoint, logsEndpoint) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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;
|
|
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
|
+
*/
|
|
400
454
|
function startFetchInstrumentation(option, hasTraces, tracesEndpoint, logsEndpoint) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
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
|
+
*/
|
|
415
484
|
function setupOtel(options) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const logProcessors = logsEndpoint ? [
|
|
464
|
-
new BatchLogRecordProcessor(
|
|
465
|
-
new OTLPLogExporter({
|
|
466
|
-
url: logsEndpoint,
|
|
467
|
-
headers: hasHeaders ? mergedHeaders : void 0
|
|
468
|
-
})
|
|
469
|
-
)
|
|
470
|
-
] : [];
|
|
471
|
-
const loggerProvider = new LoggerProvider({
|
|
472
|
-
resource,
|
|
473
|
-
processors: logProcessors
|
|
474
|
-
});
|
|
475
|
-
logs2.setGlobalLoggerProvider(loggerProvider);
|
|
476
|
-
const handle = {
|
|
477
|
-
async shutdown() {
|
|
478
|
-
fetchInstrumentation?.unpatch();
|
|
479
|
-
await Promise.allSettled([
|
|
480
|
-
tracerProvider.shutdown(),
|
|
481
|
-
loggerProvider.shutdown()
|
|
482
|
-
]);
|
|
483
|
-
activeHandle = void 0;
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
activeHandle = handle;
|
|
487
|
-
return handle;
|
|
488
|
-
}
|
|
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;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Read-only accessor for tests / debug paths that need to know whether
|
|
530
|
+
* `setupOtel` has already run in this process.
|
|
531
|
+
*/
|
|
489
532
|
function isOtelActive() {
|
|
490
|
-
|
|
533
|
+
return activeHandle !== void 0;
|
|
491
534
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
function
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return scopedTracer2;
|
|
504
|
-
}
|
|
505
|
-
function toAttributes2(attrs) {
|
|
506
|
-
const out = {};
|
|
507
|
-
for (const [k, v] of Object.entries(attrs)) {
|
|
508
|
-
if (v !== void 0) {
|
|
509
|
-
out[k] = v;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
return out;
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/with-span.ts
|
|
537
|
+
let scopedTracer;
|
|
538
|
+
function getTracer() {
|
|
539
|
+
if (!scopedTracer) scopedTracer = trace.getTracer("@photon-ai/otel", PHOTON_OTEL_VERSION);
|
|
540
|
+
return scopedTracer;
|
|
541
|
+
}
|
|
542
|
+
function toAttributes(attrs) {
|
|
543
|
+
const out = {};
|
|
544
|
+
for (const [k, v] of Object.entries(attrs)) if (v !== void 0) out[k] = v;
|
|
545
|
+
return out;
|
|
513
546
|
}
|
|
514
547
|
function withSpan(name, attrsOrFn, maybeFn) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
export {
|
|
543
|
-
PHOTON_OTEL_VERSION,
|
|
544
|
-
createLogger,
|
|
545
|
-
getLogLevel,
|
|
546
|
-
instrumentFetch,
|
|
547
|
-
isOtelActive,
|
|
548
|
-
sanitizeEmail,
|
|
549
|
-
sanitizeErrorMessage,
|
|
550
|
-
sanitizePhone,
|
|
551
|
-
setLogLevel,
|
|
552
|
-
setupOtel,
|
|
553
|
-
withSpan
|
|
554
|
-
};
|
|
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
|
+
|
|
555
574
|
//# sourceMappingURL=index.js.map
|