@stainlessdev/xray-core 0.1.0-branch.bg-publish-to-npm.18b1cb1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1131 @@
1
+ import {
2
+ bindContext,
3
+ getContextState,
4
+ logWithLevel,
5
+ sanitizeHeaderValues,
6
+ sanitizeLogString
7
+ } from "./chunk-SQHI5JZH.js";
8
+
9
+ // src/route.ts
10
+ function normalizeRoutePattern(route) {
11
+ if (!route) {
12
+ return "/";
13
+ }
14
+ const cleaned = stripQueryAndFragment(route).trim();
15
+ if (!cleaned) {
16
+ return "/";
17
+ }
18
+ const leading = cleaned.startsWith("/") ? cleaned : `/${cleaned}`;
19
+ const segments = leading.split("/").filter(Boolean);
20
+ if (segments.length === 0) {
21
+ return "/";
22
+ }
23
+ const normalized = segments.map(normalizeRouteSegment).join("/");
24
+ return `/${normalized}`;
25
+ }
26
+ function stripQueryAndFragment(value) {
27
+ const hashIndex = value.indexOf("#");
28
+ const beforeHash = hashIndex >= 0 ? value.slice(0, hashIndex) : value;
29
+ const queryIndex = beforeHash.indexOf("?");
30
+ return queryIndex >= 0 ? beforeHash.slice(0, queryIndex) : beforeHash;
31
+ }
32
+ function normalizeRouteSegment(segment) {
33
+ if (segment === "*") {
34
+ return "*";
35
+ }
36
+ if (segment.startsWith("[") && segment.endsWith("]")) {
37
+ const inner = segment.slice(1, -1);
38
+ if (inner.startsWith("...") || inner.startsWith("[...")) {
39
+ return "*";
40
+ }
41
+ return `:${inner}`;
42
+ }
43
+ if (segment.startsWith("{") && segment.endsWith("}")) {
44
+ return `:${segment.slice(1, -1)}`;
45
+ }
46
+ if (segment.startsWith("$")) {
47
+ return `:${segment.slice(1)}`;
48
+ }
49
+ if (segment.startsWith(":")) {
50
+ return `:${segment.slice(1)}`;
51
+ }
52
+ return segment;
53
+ }
54
+
55
+ // src/config.ts
56
+ var defaultCapture = {
57
+ requestHeaders: true,
58
+ responseHeaders: true,
59
+ requestBody: "none",
60
+ responseBody: "none",
61
+ maxBodyBytes: 65536
62
+ };
63
+ var defaultRedaction = {
64
+ headers: ["authorization", "cookie", "set-cookie", "x-api-key"],
65
+ queryParams: [],
66
+ bodyJsonPaths: [],
67
+ replacement: "[REDACTED]"
68
+ };
69
+ var defaultRequestId = {
70
+ header: "x-request-id",
71
+ generate: true
72
+ };
73
+ var defaultRoute = {
74
+ normalize: true,
75
+ normalizer: normalizeRoutePattern
76
+ };
77
+ var defaultOtelBase = {
78
+ endpointUrl: void 0,
79
+ headers: {},
80
+ timeoutMs: 5e3,
81
+ spanProcessor: "batch",
82
+ sampler: { type: "ratio", ratio: 1 }
83
+ };
84
+ var XrayConfigError = class extends Error {
85
+ constructor(code, message) {
86
+ super(message);
87
+ this.code = code;
88
+ }
89
+ };
90
+ function normalizeConfig(config) {
91
+ if (!config || !config.serviceName || !config.serviceName.trim()) {
92
+ throw new XrayConfigError("INVALID_CONFIG", "serviceName is required");
93
+ }
94
+ const logger = config.logger ?? console;
95
+ const logLevel = config.logLevel ?? "warn";
96
+ const capture = normalizeCapture(config.capture);
97
+ const redaction = normalizeRedaction(config.redaction);
98
+ const requestId = normalizeRequestId(config.requestId);
99
+ const route = normalizeRoute(config.route);
100
+ const otel = normalizeOtel(config.otel);
101
+ return {
102
+ serviceName: config.serviceName.trim(),
103
+ environment: config.environment?.trim() || void 0,
104
+ version: config.version?.trim() || void 0,
105
+ logger,
106
+ logLevel,
107
+ otel,
108
+ capture,
109
+ redaction,
110
+ requestId,
111
+ route
112
+ };
113
+ }
114
+ function normalizeCapture(cfg) {
115
+ const capture = {
116
+ ...defaultCapture,
117
+ ...cfg
118
+ };
119
+ if (!["none", "text", "base64"].includes(capture.requestBody)) {
120
+ throw new XrayConfigError(
121
+ "INVALID_CONFIG",
122
+ "capture.requestBody must be none, text, or base64"
123
+ );
124
+ }
125
+ if (!["none", "text", "base64"].includes(capture.responseBody)) {
126
+ throw new XrayConfigError(
127
+ "INVALID_CONFIG",
128
+ "capture.responseBody must be none, text, or base64"
129
+ );
130
+ }
131
+ if (!Number.isFinite(capture.maxBodyBytes) || capture.maxBodyBytes < 0) {
132
+ throw new XrayConfigError("INVALID_CONFIG", "capture.maxBodyBytes must be >= 0");
133
+ }
134
+ return capture;
135
+ }
136
+ function normalizeRedaction(cfg) {
137
+ const redaction = {
138
+ ...defaultRedaction,
139
+ ...cfg
140
+ };
141
+ redaction.headers = normalizeStringList(redaction.headers);
142
+ redaction.queryParams = normalizeStringList(redaction.queryParams);
143
+ redaction.bodyJsonPaths = normalizeStringList(redaction.bodyJsonPaths);
144
+ redaction.replacement = redaction.replacement || defaultRedaction.replacement;
145
+ if (!redaction.replacement) {
146
+ throw new XrayConfigError("INVALID_REDACTION", "redaction.replacement must be non-empty");
147
+ }
148
+ return redaction;
149
+ }
150
+ function normalizeRequestId(cfg) {
151
+ const requestId = {
152
+ ...defaultRequestId,
153
+ ...cfg
154
+ };
155
+ requestId.header = requestId.header.trim().toLowerCase();
156
+ if (!requestId.header) {
157
+ throw new XrayConfigError("INVALID_CONFIG", "requestId.header must be non-empty");
158
+ }
159
+ return requestId;
160
+ }
161
+ function normalizeRoute(cfg) {
162
+ const route = {
163
+ ...defaultRoute,
164
+ ...cfg
165
+ };
166
+ if (route.normalize && !route.normalizer) {
167
+ route.normalizer = normalizeRoutePattern;
168
+ }
169
+ return route;
170
+ }
171
+ function normalizeOtel(cfg) {
172
+ const endpointUrl = cfg?.endpointUrl?.trim() || void 0;
173
+ const enabled = cfg?.enabled ?? !!endpointUrl;
174
+ const otel = {
175
+ ...defaultOtelBase,
176
+ ...cfg,
177
+ enabled,
178
+ endpointUrl,
179
+ headers: cfg?.headers ?? defaultOtelBase.headers,
180
+ timeoutMs: cfg?.timeoutMs ?? defaultOtelBase.timeoutMs,
181
+ spanProcessor: cfg?.spanProcessor ?? defaultOtelBase.spanProcessor,
182
+ sampler: cfg?.sampler ?? defaultOtelBase.sampler
183
+ };
184
+ if (otel.enabled && !otel.endpointUrl) {
185
+ throw new XrayConfigError("INVALID_OTEL", "otel.endpointUrl is required when enabled");
186
+ }
187
+ if (otel.sampler.type === "ratio") {
188
+ const ratio = otel.sampler.ratio ?? 1;
189
+ if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
190
+ throw new XrayConfigError("INVALID_OTEL", "otel.sampler.ratio must be between 0 and 1");
191
+ }
192
+ otel.sampler = { type: "ratio", ratio };
193
+ }
194
+ return otel;
195
+ }
196
+ function normalizeStringList(values) {
197
+ if (!values) {
198
+ return [];
199
+ }
200
+ return values.map((entry) => entry.trim()).filter(Boolean);
201
+ }
202
+
203
+ // src/header_redaction.ts
204
+ var headerNameCompactor = /[-_.]/g;
205
+ var defaultHeaderMatcher = newHeaderRedactionMatcher(
206
+ defaultSensitiveHeaderNames(),
207
+ defaultSensitiveKeywords()
208
+ );
209
+ function authSchemePrefix(value) {
210
+ if (!value) {
211
+ return "";
212
+ }
213
+ const lower = value.toLowerCase();
214
+ if (lower.startsWith("basic")) {
215
+ return value.slice(0, "basic".length);
216
+ }
217
+ if (lower.startsWith("bearer")) {
218
+ return value.slice(0, "bearer".length);
219
+ }
220
+ if (lower.startsWith("digest")) {
221
+ return value.slice(0, "digest".length);
222
+ }
223
+ if (lower.startsWith("negotiate")) {
224
+ return value.slice(0, "negotiate".length);
225
+ }
226
+ return "";
227
+ }
228
+ function redactCookieValue(value, replacement) {
229
+ if (!value) {
230
+ return replacement;
231
+ }
232
+ const parts = value.split(";");
233
+ const redacted = [];
234
+ for (const part of parts) {
235
+ const segment = part.trim();
236
+ if (!segment) {
237
+ redacted.push(replacement);
238
+ continue;
239
+ }
240
+ const idx = segment.indexOf("=");
241
+ if (idx <= 0) {
242
+ redacted.push(replacement);
243
+ continue;
244
+ }
245
+ const name = segment.slice(0, idx);
246
+ if (!name) {
247
+ redacted.push(replacement);
248
+ continue;
249
+ }
250
+ redacted.push(`${name}=${replacement}`);
251
+ }
252
+ return redacted.join("; ");
253
+ }
254
+ function redactSetCookieValue(value, replacement) {
255
+ if (!value) {
256
+ return replacement;
257
+ }
258
+ const parts = value.split(";");
259
+ const first = parts.shift() ?? "";
260
+ const idx = first.indexOf("=");
261
+ if (idx <= 0) {
262
+ return replacement;
263
+ }
264
+ const name = first.slice(0, idx);
265
+ if (!name) {
266
+ return replacement;
267
+ }
268
+ const redacted = `${name}=${replacement}`;
269
+ if (parts.length === 0) {
270
+ return redacted;
271
+ }
272
+ return `${redacted};${parts.join(";")}`;
273
+ }
274
+ function addSensitiveHeaderNames(target, headers) {
275
+ for (const header of headers) {
276
+ const normalized = normalizeHeaderName(header);
277
+ if (normalized) {
278
+ target.add(normalized);
279
+ }
280
+ }
281
+ }
282
+ function buildKeywordSets(keywords) {
283
+ const tokens = /* @__PURE__ */ new Set();
284
+ const compacted = /* @__PURE__ */ new Set();
285
+ for (const keyword of keywords) {
286
+ const normalized = normalizeHeaderName(keyword);
287
+ if (!normalized) {
288
+ continue;
289
+ }
290
+ const compactedKeyword = compactNormalizedHeaderName(normalized);
291
+ if (compactedKeyword) {
292
+ compacted.add(compactedKeyword);
293
+ }
294
+ if (!normalized.includes("-") && !normalized.includes("_") && !normalized.includes(".")) {
295
+ tokens.add(normalized);
296
+ }
297
+ }
298
+ return {
299
+ compacted: Array.from(compacted).sort(),
300
+ tokens
301
+ };
302
+ }
303
+ function compactNormalizedHeaderName(normalized) {
304
+ if (!normalized) {
305
+ return "";
306
+ }
307
+ return normalized.replace(headerNameCompactor, "");
308
+ }
309
+ function defaultSensitiveHeaderNames() {
310
+ return [
311
+ "authorization",
312
+ "cookie",
313
+ "proxy-authenticate",
314
+ "proxy-authorization",
315
+ "set-cookie",
316
+ "www-authenticate"
317
+ ];
318
+ }
319
+ function defaultSensitiveKeywords() {
320
+ return [
321
+ "api-key",
322
+ "api_key",
323
+ "apikey",
324
+ "auth",
325
+ "authenticate",
326
+ "authorization",
327
+ "credential",
328
+ "password",
329
+ "passwd",
330
+ "private-key",
331
+ "private_key",
332
+ "privatekey",
333
+ "secret",
334
+ "session",
335
+ "sessionid",
336
+ "signature",
337
+ "token"
338
+ ];
339
+ }
340
+ function newHeaderRedactionMatcher(names, keywords) {
341
+ const exactSensitive = /* @__PURE__ */ new Set();
342
+ addSensitiveHeaderNames(exactSensitive, names);
343
+ const { compacted, tokens } = buildKeywordSets(keywords);
344
+ return {
345
+ exactSensitive,
346
+ keywordCompacted: compacted,
347
+ keywordTokens: tokens
348
+ };
349
+ }
350
+ function normalizeHeaderName(name) {
351
+ return name.trim().toLowerCase();
352
+ }
353
+
354
+ // src/redaction.ts
355
+ function applyRedaction(config, log) {
356
+ const redacted = { ...log };
357
+ if (redacted.requestHeaders) {
358
+ redacted.requestHeaders = redactHeaders(redacted.requestHeaders, config);
359
+ }
360
+ if (redacted.responseHeaders) {
361
+ redacted.responseHeaders = redactHeaders(redacted.responseHeaders, config);
362
+ }
363
+ redacted.url = redactUrl(redacted.url, config);
364
+ if (redacted.requestBody) {
365
+ redacted.requestBody = redactBody(redacted.requestBody, redacted.requestHeaders, config);
366
+ }
367
+ if (redacted.responseBody) {
368
+ redacted.responseBody = redactBody(redacted.responseBody, redacted.responseHeaders, config);
369
+ }
370
+ return redacted;
371
+ }
372
+ function redactHeaders(headers, config) {
373
+ const list = new Set(config.headers.map((name) => name.toLowerCase()));
374
+ const result = {};
375
+ for (const [name, value] of Object.entries(headers)) {
376
+ const lower = name.toLowerCase();
377
+ if (!list.has(lower)) {
378
+ result[name] = value;
379
+ continue;
380
+ }
381
+ const redactValue = (entry) => redactHeaderValue(lower, entry, config.replacement);
382
+ if (Array.isArray(value)) {
383
+ result[name] = value.map(redactValue);
384
+ } else {
385
+ result[name] = redactValue(value);
386
+ }
387
+ }
388
+ return result;
389
+ }
390
+ function redactHeaderValue(name, value, replacement) {
391
+ switch (name) {
392
+ case "authorization":
393
+ case "proxy-authorization": {
394
+ const scheme = authSchemePrefix(value);
395
+ if (!scheme) {
396
+ return replacement;
397
+ }
398
+ return `${scheme} ${replacement}`;
399
+ }
400
+ case "cookie":
401
+ return redactCookieValue(value, replacement);
402
+ case "set-cookie":
403
+ return redactSetCookieValue(value, replacement);
404
+ default:
405
+ return replacement;
406
+ }
407
+ }
408
+ function redactUrl(value, config) {
409
+ if (!value || config.queryParams.length === 0) {
410
+ return value;
411
+ }
412
+ try {
413
+ const parsed = new URL(value);
414
+ const params = parsed.searchParams;
415
+ if (!params || params.size === 0) {
416
+ return value;
417
+ }
418
+ const redact = new Set(config.queryParams.map((key) => key.toLowerCase()));
419
+ const next = new URLSearchParams();
420
+ params.forEach((val, key) => {
421
+ if (redact.has(key.toLowerCase())) {
422
+ next.append(key, config.replacement);
423
+ } else {
424
+ next.append(key, val);
425
+ }
426
+ });
427
+ parsed.search = next.toString();
428
+ return parsed.toString();
429
+ } catch {
430
+ return value;
431
+ }
432
+ }
433
+ function redactBody(body, headers, config) {
434
+ if (config.bodyJsonPaths.length === 0) {
435
+ return body;
436
+ }
437
+ if (!body.value || body.encoding !== "utf8") {
438
+ return body;
439
+ }
440
+ if (!isJsonContentType(headers)) {
441
+ return body;
442
+ }
443
+ let parsed;
444
+ try {
445
+ parsed = JSON.parse(body.value);
446
+ } catch {
447
+ return body;
448
+ }
449
+ for (const path of config.bodyJsonPaths) {
450
+ const segments = parseJsonPath(path);
451
+ if (!segments) {
452
+ continue;
453
+ }
454
+ redactJsonPath(parsed, segments, config.replacement);
455
+ }
456
+ const next = { ...body };
457
+ next.value = JSON.stringify(parsed);
458
+ return next;
459
+ }
460
+ function isJsonContentType(headers) {
461
+ if (!headers) {
462
+ return false;
463
+ }
464
+ const contentType = headers["content-type"] || headers["Content-Type"];
465
+ const value = Array.isArray(contentType) ? contentType[0] : contentType;
466
+ if (!value) {
467
+ return false;
468
+ }
469
+ const normalized = value.split(";")[0];
470
+ if (!normalized) {
471
+ return false;
472
+ }
473
+ const trimmed = normalized.trim().toLowerCase();
474
+ return trimmed === "application/json" || trimmed.endsWith("+json");
475
+ }
476
+ function parseJsonPath(path) {
477
+ const trimmed = path.trim();
478
+ if (!trimmed) {
479
+ return null;
480
+ }
481
+ const normalized = trimmed.startsWith("$.") ? trimmed.slice(2) : trimmed;
482
+ if (!normalized) {
483
+ return null;
484
+ }
485
+ const segments = [];
486
+ const parts = normalized.split(".");
487
+ for (const part of parts) {
488
+ if (!part) {
489
+ continue;
490
+ }
491
+ let cursor = part;
492
+ const bracketIndex = cursor.indexOf("[");
493
+ if (bracketIndex === -1) {
494
+ segments.push(cursor);
495
+ continue;
496
+ }
497
+ const name = cursor.slice(0, bracketIndex);
498
+ if (name) {
499
+ segments.push(name);
500
+ }
501
+ cursor = cursor.slice(bracketIndex);
502
+ const matches = cursor.match(/\[(\d+)\]/g);
503
+ if (!matches) {
504
+ continue;
505
+ }
506
+ for (const match of matches) {
507
+ const indexValue = match.slice(1, -1);
508
+ const index = Number.parseInt(indexValue, 10);
509
+ if (Number.isFinite(index)) {
510
+ segments.push(index);
511
+ }
512
+ }
513
+ }
514
+ return segments.length > 0 ? segments : null;
515
+ }
516
+ function redactJsonPath(value, segments, replacement) {
517
+ if (!value || segments.length === 0) {
518
+ return;
519
+ }
520
+ let current = value;
521
+ for (let i = 0; i < segments.length; i += 1) {
522
+ const segment = segments[i];
523
+ if (segment === void 0) {
524
+ return;
525
+ }
526
+ const isLast = i === segments.length - 1;
527
+ if (typeof segment === "number") {
528
+ if (!Array.isArray(current) || segment < 0 || segment >= current.length) {
529
+ return;
530
+ }
531
+ if (isLast) {
532
+ current[segment] = replacement;
533
+ return;
534
+ }
535
+ current = current[segment];
536
+ continue;
537
+ }
538
+ if (!current || typeof current !== "object") {
539
+ return;
540
+ }
541
+ const record = current;
542
+ if (!(segment in record)) {
543
+ return;
544
+ }
545
+ if (isLast) {
546
+ record[segment] = replacement;
547
+ return;
548
+ }
549
+ current = record[segment];
550
+ }
551
+ }
552
+
553
+ // src/attrkey.ts
554
+ var AttributeKeyRequestBody = "http.request.body";
555
+ var AttributeKeyRequestBodyEncoding = "http.request.body.encoding";
556
+ var AttributeKeyRequestBodyTruncated = "http.request.body.truncated";
557
+ var AttributeKeyRequestID = "http.request.id";
558
+ var AttributeKeyResponseBody = "http.response.body";
559
+ var AttributeKeyResponseBodyEncoding = "http.response.body.encoding";
560
+ var AttributeKeyResponseBodyTruncated = "http.response.body.truncated";
561
+ var AttributeKeySpanDrop = "stainlessxray.internal.drop";
562
+
563
+ // src/attributes.ts
564
+ var attributeKeyEndUserId = "enduser.id";
565
+ var attributeKeyHttpRequestBodySize = "http.request.body.size";
566
+ var attributeKeyHttpRequestMethod = "http.request.method";
567
+ var attributeKeyHttpResponseBodySize = "http.response.body.size";
568
+ var attributeKeyHttpResponseStatusCode = "http.response.status_code";
569
+ var attributeKeyHttpRoute = "http.route";
570
+ var attributeKeyUrlPath = "url.path";
571
+ var attributeKeyUrlFull = "url.full";
572
+ function setHeaderAttributes(span, headers, prefix) {
573
+ if (!headers) {
574
+ return;
575
+ }
576
+ const keys = Object.keys(headers).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
577
+ for (const key of keys) {
578
+ const values = headers[key];
579
+ if (!values || Array.isArray(values) && values.length === 0) {
580
+ continue;
581
+ }
582
+ span.setAttribute(prefix + key.toLowerCase(), Array.isArray(values) ? values : [values]);
583
+ }
584
+ }
585
+ function setRequestAttributes(span, method, urlFull) {
586
+ span.setAttribute(attributeKeyHttpRequestMethod, method);
587
+ if (urlFull) {
588
+ span.setAttribute(attributeKeyUrlFull, urlFull);
589
+ const path = extractPath(urlFull);
590
+ if (path) {
591
+ span.setAttribute(attributeKeyUrlPath, path);
592
+ }
593
+ }
594
+ }
595
+ function extractPath(url) {
596
+ try {
597
+ return new URL(url).pathname;
598
+ } catch {
599
+ const match = url.match(/^[^?#]*/);
600
+ return match?.[0] || void 0;
601
+ }
602
+ }
603
+ function setRequestBodyAttributes(span, body) {
604
+ if (!body.value) {
605
+ return;
606
+ }
607
+ span.setAttribute(AttributeKeyRequestBody, body.value);
608
+ span.setAttribute(AttributeKeyRequestBodyEncoding, body.encoding);
609
+ if (body.truncated) {
610
+ span.setAttribute(AttributeKeyRequestBodyTruncated, true);
611
+ }
612
+ }
613
+ function setRequestBodySizeAttribute(span, size) {
614
+ span.setAttribute(attributeKeyHttpRequestBodySize, size);
615
+ }
616
+ function setResponseBodyAttributes(span, body) {
617
+ if (!body.value) {
618
+ return;
619
+ }
620
+ span.setAttribute(AttributeKeyResponseBody, body.value);
621
+ span.setAttribute(AttributeKeyResponseBodyEncoding, body.encoding);
622
+ if (body.truncated) {
623
+ span.setAttribute(AttributeKeyResponseBodyTruncated, true);
624
+ }
625
+ }
626
+ function setResponseBodySizeAttribute(span, size) {
627
+ span.setAttribute(attributeKeyHttpResponseBodySize, size);
628
+ }
629
+ function setResponseStatusAttribute(span, statusCode) {
630
+ span.setAttribute(attributeKeyHttpResponseStatusCode, statusCode);
631
+ }
632
+ function setRouteAttribute(span, route) {
633
+ if (route) {
634
+ span.setAttribute(attributeKeyHttpRoute, route);
635
+ }
636
+ }
637
+ function setUserIdAttribute(span, userId) {
638
+ span.setAttribute(attributeKeyEndUserId, userId);
639
+ }
640
+ function setRequestIdAttribute(span, requestId) {
641
+ span.setAttribute(AttributeKeyRequestID, requestId);
642
+ }
643
+
644
+ // src/otel.ts
645
+ import {
646
+ diag,
647
+ ROOT_CONTEXT,
648
+ SpanKind,
649
+ SpanStatusCode
650
+ } from "@opentelemetry/api";
651
+ import { ExportResultCode } from "@opentelemetry/core";
652
+ import {
653
+ AlwaysOffSampler,
654
+ AlwaysOnSampler,
655
+ BasicTracerProvider,
656
+ BatchSpanProcessor,
657
+ SimpleSpanProcessor,
658
+ TraceIdRatioBasedSampler
659
+ } from "@opentelemetry/sdk-trace-base";
660
+ import { resourceFromAttributes } from "@opentelemetry/resources";
661
+ import {
662
+ ATTR_SERVICE_NAME,
663
+ ATTR_TELEMETRY_SDK_LANGUAGE,
664
+ ATTR_TELEMETRY_SDK_NAME,
665
+ ATTR_TELEMETRY_SDK_VERSION
666
+ } from "@opentelemetry/semantic-conventions";
667
+ import { JsonTraceSerializer, ProtobufTraceSerializer } from "@opentelemetry/otlp-transformer";
668
+ var defaultAttributeCountLimit = 128;
669
+ function createTracerProvider(config) {
670
+ if (!config.otel.enabled || !config.otel.endpointUrl) {
671
+ return null;
672
+ }
673
+ if (config.otel.endpointUrl.startsWith("http://")) {
674
+ diag.warn("xray: OTLP endpoint uses plaintext HTTP");
675
+ }
676
+ const attributeValueLengthLimit = Math.max(1, Math.ceil(config.capture.maxBodyBytes * 4 / 3));
677
+ const resource = resourceFromAttributes({
678
+ [ATTR_SERVICE_NAME]: config.serviceName,
679
+ [ATTR_TELEMETRY_SDK_LANGUAGE]: isNodeRuntime() ? "nodejs" : "webjs",
680
+ [ATTR_TELEMETRY_SDK_NAME]: "stainless-xray",
681
+ [ATTR_TELEMETRY_SDK_VERSION]: sdkVersion()
682
+ });
683
+ const exporter = new FetchSpanExporter({
684
+ endpointUrl: config.otel.endpointUrl,
685
+ headers: config.otel.headers ?? {},
686
+ timeoutMillis: config.otel.timeoutMs
687
+ });
688
+ const sampler = createSampler(config);
689
+ const spanProcessor = createSpanProcessor(config.otel.spanProcessor, exporter);
690
+ const dropProcessor = new DropFilterSpanProcessor(spanProcessor);
691
+ const provider = new BasicTracerProvider({
692
+ forceFlushTimeoutMillis: 3e4,
693
+ generalLimits: {
694
+ attributeCountLimit: defaultAttributeCountLimit,
695
+ attributeValueLengthLimit
696
+ },
697
+ resource,
698
+ sampler,
699
+ spanLimits: {
700
+ attributeCountLimit: defaultAttributeCountLimit,
701
+ attributePerEventCountLimit: defaultAttributeCountLimit,
702
+ attributePerLinkCountLimit: defaultAttributeCountLimit,
703
+ attributeValueLengthLimit,
704
+ eventCountLimit: defaultAttributeCountLimit,
705
+ linkCountLimit: defaultAttributeCountLimit
706
+ },
707
+ spanProcessors: [dropProcessor]
708
+ });
709
+ return provider;
710
+ }
711
+ function tracerFromProvider(provider) {
712
+ return provider.getTracer("stainless-xray");
713
+ }
714
+ function spanFromTracer(tracer, name) {
715
+ return tracer.startSpan(name, { kind: SpanKind.SERVER }, ROOT_CONTEXT);
716
+ }
717
+ function spanStatusFromError(span, err) {
718
+ if (err instanceof Error) {
719
+ span.recordException(err);
720
+ } else if (typeof err === "string") {
721
+ span.recordException(err);
722
+ } else {
723
+ span.recordException({ message: String(err) });
724
+ }
725
+ span.setStatus({ code: SpanStatusCode.ERROR });
726
+ }
727
+ var DropFilterSpanProcessor = class {
728
+ constructor(next) {
729
+ this.next = next;
730
+ }
731
+ forceFlush() {
732
+ return this.next.forceFlush();
733
+ }
734
+ onEnd(span) {
735
+ if (span.attributes[AttributeKeySpanDrop] === true) {
736
+ return;
737
+ }
738
+ this.next.onEnd(span);
739
+ }
740
+ onStart(span, parentContext) {
741
+ this.next.onStart(span, parentContext);
742
+ }
743
+ shutdown() {
744
+ return this.next.shutdown();
745
+ }
746
+ };
747
+ var FetchSpanExporter = class {
748
+ constructor(options) {
749
+ this.endpointUrl = options.endpointUrl;
750
+ this.headers = { ...options.headers };
751
+ this.timeoutMillis = options.timeoutMillis;
752
+ this.isShutdown = false;
753
+ const protobufSerializer = ProtobufTraceSerializer && typeof ProtobufTraceSerializer.serializeRequest === "function" ? ProtobufTraceSerializer : null;
754
+ this.serializer = protobufSerializer ?? JsonTraceSerializer;
755
+ this.contentType = protobufSerializer ? "application/x-protobuf" : "application/json";
756
+ }
757
+ export(spans, resultCallback) {
758
+ if (this.isShutdown) {
759
+ resultCallback({ code: ExportResultCode.FAILED });
760
+ return;
761
+ }
762
+ const payload = this.serializer.serializeRequest(spans);
763
+ if (!payload) {
764
+ resultCallback({
765
+ code: ExportResultCode.FAILED,
766
+ error: new Error("OTLP export failed: empty payload")
767
+ });
768
+ return;
769
+ }
770
+ const headers = {
771
+ ...this.headers,
772
+ "Content-Type": this.contentType
773
+ };
774
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
775
+ let timeout;
776
+ if (controller) {
777
+ timeout = setTimeout(() => controller.abort(), this.timeoutMillis);
778
+ }
779
+ const doExport = async () => {
780
+ const response = await fetch(this.endpointUrl, {
781
+ method: "POST",
782
+ headers,
783
+ body: payload,
784
+ signal: controller?.signal
785
+ });
786
+ if (!response.ok) {
787
+ throw new Error(`OTLP export failed: ${response.status}`);
788
+ }
789
+ };
790
+ doExport().then(() => {
791
+ if (timeout) {
792
+ clearTimeout(timeout);
793
+ }
794
+ resultCallback({ code: ExportResultCode.SUCCESS });
795
+ }).catch((err) => {
796
+ if (timeout) {
797
+ clearTimeout(timeout);
798
+ }
799
+ diag.error("OTLP export failed", err);
800
+ resultCallback({ code: ExportResultCode.FAILED, error: err });
801
+ });
802
+ }
803
+ async forceFlush() {
804
+ return;
805
+ }
806
+ async shutdown() {
807
+ this.isShutdown = true;
808
+ }
809
+ };
810
+ function createSpanProcessor(mode, exporter) {
811
+ if (mode === "simple") {
812
+ return new SimpleSpanProcessor(exporter);
813
+ }
814
+ return new BatchSpanProcessor(exporter, {
815
+ maxQueueSize: 2048,
816
+ maxExportBatchSize: 512,
817
+ scheduledDelayMillis: 5e3,
818
+ exportTimeoutMillis: 3e4
819
+ });
820
+ }
821
+ function createSampler(config) {
822
+ const sampler = config.otel.sampler;
823
+ if (sampler.type === "always_off") {
824
+ return new AlwaysOffSampler();
825
+ }
826
+ if (sampler.type === "ratio") {
827
+ return new TraceIdRatioBasedSampler(sampler.ratio ?? 1);
828
+ }
829
+ return new AlwaysOnSampler();
830
+ }
831
+ function sdkVersion() {
832
+ if (true) {
833
+ return "0.1.0";
834
+ }
835
+ return "unknown";
836
+ }
837
+ function isNodeRuntime() {
838
+ const maybeProcess = globalThis.process;
839
+ return !!maybeProcess?.versions?.node;
840
+ }
841
+
842
+ // src/uuid.ts
843
+ function uuidv7() {
844
+ const bytes = new Uint8Array(16);
845
+ crypto.getRandomValues(bytes);
846
+ const timestamp = BigInt(Date.now());
847
+ bytes[0] = Number(timestamp >> 40n & 0xffn);
848
+ bytes[1] = Number(timestamp >> 32n & 0xffn);
849
+ bytes[2] = Number(timestamp >> 24n & 0xffn);
850
+ bytes[3] = Number(timestamp >> 16n & 0xffn);
851
+ bytes[4] = Number(timestamp >> 8n & 0xffn);
852
+ bytes[5] = Number(timestamp & 0xffn);
853
+ const byte6 = bytes[6] ?? 0;
854
+ const byte8 = bytes[8] ?? 0;
855
+ bytes[6] = byte6 & 15 | 112;
856
+ bytes[8] = byte8 & 63 | 128;
857
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
858
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
859
+ }
860
+
861
+ // src/emitter.ts
862
+ function createEmitter(config) {
863
+ const resolved = normalizeConfig(config);
864
+ const tracerProvider = createTracerProvider(resolved);
865
+ const tracer = tracerProvider ? tracerFromProvider(tracerProvider) : null;
866
+ return {
867
+ config: resolved,
868
+ startRequest: (req) => startRequest(resolved, tracer, req),
869
+ endRequest: (ctx, res, err) => endRequest(resolved, ctx, res, err),
870
+ flush: () => tracerProvider ? tracerProvider.forceFlush() : Promise.resolve(),
871
+ shutdown: () => tracerProvider ? tracerProvider.shutdown() : Promise.resolve()
872
+ };
873
+ }
874
+ function startRequest(config, tracer, req) {
875
+ const startTimeMs = Number.isFinite(req.startTimeMs) ? req.startTimeMs : Date.now();
876
+ req.startTimeMs = startTimeMs;
877
+ const requestId = resolveRequestId(config, req.requestId, req.headers);
878
+ req.requestId = requestId;
879
+ if (req.route && config.route.normalize) {
880
+ req.route = config.route.normalizer ? config.route.normalizer(req.route) : normalizeRoutePattern(req.route);
881
+ }
882
+ const span = tracer ? spanFromTracer(tracer, spanNameFromRequest(req)) : void 0;
883
+ const context = {
884
+ requestId,
885
+ traceId: span?.spanContext().traceId,
886
+ spanId: span?.spanContext().spanId,
887
+ setUserId: (id) => {
888
+ const state2 = getContextState(context);
889
+ if (!state2) {
890
+ return;
891
+ }
892
+ state2.userId = id;
893
+ if (span && id) {
894
+ try {
895
+ setUserIdAttribute(span, id);
896
+ } catch {
897
+ }
898
+ }
899
+ },
900
+ setSessionId: (id) => {
901
+ const state2 = getContextState(context);
902
+ if (!state2) {
903
+ return;
904
+ }
905
+ state2.sessionId = id;
906
+ },
907
+ setAttribute: (key, value) => {
908
+ const state2 = getContextState(context);
909
+ if (!state2) {
910
+ return;
911
+ }
912
+ state2.attributes[key] = value;
913
+ if (span) {
914
+ try {
915
+ span.setAttribute(key, value);
916
+ } catch {
917
+ }
918
+ }
919
+ },
920
+ addEvent: (name, attributes) => {
921
+ const state2 = getContextState(context);
922
+ if (!state2) {
923
+ return;
924
+ }
925
+ state2.events.push({ name, attributes });
926
+ if (span) {
927
+ try {
928
+ span.addEvent(name, attributes);
929
+ } catch {
930
+ }
931
+ }
932
+ },
933
+ setError: (err) => {
934
+ const state2 = getContextState(context);
935
+ if (!state2) {
936
+ return;
937
+ }
938
+ state2.error = err;
939
+ if (span) {
940
+ try {
941
+ spanStatusFromError(span, err);
942
+ } catch {
943
+ }
944
+ }
945
+ }
946
+ };
947
+ const state = {
948
+ request: req,
949
+ config,
950
+ span,
951
+ context,
952
+ attributes: {},
953
+ events: []
954
+ };
955
+ bindContext(context, state);
956
+ return context;
957
+ }
958
+ function endRequest(config, ctx, res, err) {
959
+ const state = getContextState(ctx);
960
+ const endTimeMs = Number.isFinite(res.endTimeMs) ? res.endTimeMs : Date.now();
961
+ res.endTimeMs = endTimeMs;
962
+ if (!state) {
963
+ const fallbackLog = {
964
+ requestId: ctx.requestId,
965
+ serviceName: config.serviceName,
966
+ method: res.statusCode ? "UNKNOWN" : "UNKNOWN",
967
+ url: "",
968
+ durationMs: 0,
969
+ statusCode: res.statusCode,
970
+ timestamp: new Date(endTimeMs).toISOString()
971
+ };
972
+ return fallbackLog;
973
+ }
974
+ const request = state.request;
975
+ const capture = resolveCapture(config.capture, state.captureOverride);
976
+ const redaction = resolveRedaction(config.redaction, state.redactionOverride);
977
+ const route = request.route;
978
+ const url = sanitizeLogString(request.url);
979
+ const log = {
980
+ requestId: request.requestId ?? ctx.requestId,
981
+ traceId: state.span?.spanContext().traceId,
982
+ spanId: state.span?.spanContext().spanId,
983
+ serviceName: config.serviceName,
984
+ method: request.method,
985
+ url,
986
+ route,
987
+ statusCode: res.statusCode,
988
+ durationMs: Math.max(0, endTimeMs - request.startTimeMs),
989
+ requestHeaders: capture.requestHeaders ? sanitizeHeaderValues(request.headers) : void 0,
990
+ responseHeaders: capture.responseHeaders ? sanitizeHeaderValues(res.headers) : void 0,
991
+ requestBody: capture.requestBody === "none" ? void 0 : request.body,
992
+ responseBody: capture.responseBody === "none" ? void 0 : res.body,
993
+ userId: state.userId ?? void 0,
994
+ sessionId: state.sessionId ?? void 0,
995
+ error: buildError(err ?? state.error),
996
+ attributes: Object.keys(state.attributes).length > 0 ? { ...state.attributes } : void 0,
997
+ timestamp: new Date(endTimeMs).toISOString()
998
+ };
999
+ const redacted = applyRedaction(redaction, log);
1000
+ const span = state.span;
1001
+ if (span) {
1002
+ try {
1003
+ setRequestAttributes(span, request.method, redacted.url);
1004
+ setRequestIdAttribute(span, redacted.requestId);
1005
+ span.setAttribute("service.name", config.serviceName);
1006
+ if (redacted.statusCode != null) {
1007
+ setResponseStatusAttribute(span, redacted.statusCode);
1008
+ }
1009
+ if (redacted.route) {
1010
+ const normalized = config.route.normalize ? config.route.normalizer ? config.route.normalizer(redacted.route) : normalizeRoutePattern(redacted.route) : redacted.route;
1011
+ setRouteAttribute(span, normalized);
1012
+ span.updateName(`${request.method} ${normalized}`);
1013
+ } else {
1014
+ span.updateName(spanNameFromRequest(request));
1015
+ }
1016
+ if (redacted.requestHeaders) {
1017
+ setHeaderAttributes(span, redacted.requestHeaders, "http.request.header.");
1018
+ }
1019
+ if (redacted.responseHeaders) {
1020
+ setHeaderAttributes(span, redacted.responseHeaders, "http.response.header.");
1021
+ }
1022
+ if (redacted.requestBody) {
1023
+ setRequestBodyAttributes(span, redacted.requestBody);
1024
+ setRequestBodySizeAttribute(span, redacted.requestBody.bytes);
1025
+ }
1026
+ if (redacted.responseBody) {
1027
+ setResponseBodyAttributes(span, redacted.responseBody);
1028
+ setResponseBodySizeAttribute(span, redacted.responseBody.bytes);
1029
+ }
1030
+ if (state.userId) {
1031
+ setUserIdAttribute(span, state.userId);
1032
+ }
1033
+ if (err ?? state.error) {
1034
+ spanStatusFromError(span, err ?? state.error);
1035
+ }
1036
+ span.end();
1037
+ } catch (spanErr) {
1038
+ logWithLevel(config.logger, "warn", config.logLevel, "xray: span finalize failed", {
1039
+ error: spanErr instanceof Error ? spanErr.message : String(spanErr)
1040
+ });
1041
+ }
1042
+ }
1043
+ return redacted;
1044
+ }
1045
+ function resolveRequestId(config, requestId, headers) {
1046
+ if (requestId) {
1047
+ return requestId;
1048
+ }
1049
+ const headerName = config.requestId.header.toLowerCase();
1050
+ const headerValue = headers[headerName];
1051
+ if (headerValue) {
1052
+ const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;
1053
+ if (value && value.trim()) {
1054
+ return value.trim();
1055
+ }
1056
+ }
1057
+ if (!config.requestId.generate) {
1058
+ return uuidv7();
1059
+ }
1060
+ return uuidv7();
1061
+ }
1062
+ function resolveCapture(base, override) {
1063
+ if (!override) {
1064
+ return base;
1065
+ }
1066
+ return {
1067
+ ...base,
1068
+ ...override
1069
+ };
1070
+ }
1071
+ function resolveRedaction(base, override) {
1072
+ const merged = {
1073
+ ...base,
1074
+ ...override
1075
+ };
1076
+ merged.headers = normalizeLowercaseList(merged.headers);
1077
+ merged.queryParams = normalizeLowercaseList(merged.queryParams);
1078
+ merged.bodyJsonPaths = normalizeList(merged.bodyJsonPaths);
1079
+ merged.replacement = merged.replacement || base.replacement;
1080
+ return merged;
1081
+ }
1082
+ function normalizeLowercaseList(values) {
1083
+ if (!values) {
1084
+ return [];
1085
+ }
1086
+ return values.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
1087
+ }
1088
+ function normalizeList(values) {
1089
+ if (!values) {
1090
+ return [];
1091
+ }
1092
+ return values.map((entry) => entry.trim()).filter(Boolean);
1093
+ }
1094
+ function buildError(err) {
1095
+ if (!err) {
1096
+ return void 0;
1097
+ }
1098
+ if (err instanceof Error) {
1099
+ return {
1100
+ message: err.message || "Error",
1101
+ type: err.name || "Error",
1102
+ stack: err.stack
1103
+ };
1104
+ }
1105
+ return {
1106
+ message: String(err)
1107
+ };
1108
+ }
1109
+ function spanNameFromRequest(req) {
1110
+ const method = req.method || "GET";
1111
+ if (req.route) {
1112
+ return `${method} ${req.route}`;
1113
+ }
1114
+ const path = safePath(req.url);
1115
+ return `${method} ${path}`;
1116
+ }
1117
+ function safePath(url) {
1118
+ try {
1119
+ const parsed = new URL(url);
1120
+ return parsed.pathname || "/";
1121
+ } catch {
1122
+ const rawPath = url.split("?")[0] || "/";
1123
+ return rawPath || "/";
1124
+ }
1125
+ }
1126
+ export {
1127
+ XrayConfigError,
1128
+ createEmitter,
1129
+ normalizeConfig
1130
+ };
1131
+ //# sourceMappingURL=index.js.map