@pingops/otel 0.2.3 → 0.2.5
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.cjs +126 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +127 -44
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -60,6 +60,21 @@ function getGlobalConfig() {
|
|
|
60
60
|
//#endregion
|
|
61
61
|
//#region src/span-processor.ts
|
|
62
62
|
const logger$2 = (0, _pingops_core.createLogger)("[PingOps Processor]");
|
|
63
|
+
function normalizePath$1(pathname) {
|
|
64
|
+
return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
65
|
+
}
|
|
66
|
+
function isExporterRequestUrl$1(url$1, exporterUrl) {
|
|
67
|
+
try {
|
|
68
|
+
const request = new URL(url$1);
|
|
69
|
+
const exporter = new URL(exporterUrl);
|
|
70
|
+
if (request.origin !== exporter.origin) return false;
|
|
71
|
+
const requestPath = normalizePath$1(request.pathname);
|
|
72
|
+
const exporterPath = normalizePath$1(exporter.pathname);
|
|
73
|
+
return requestPath === exporterPath || requestPath.startsWith(`${exporterPath}/`);
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
63
78
|
/**
|
|
64
79
|
* Creates a filtered span wrapper that applies header filtering to attributes
|
|
65
80
|
*
|
|
@@ -93,6 +108,7 @@ function createFilteredSpan(span, domainAllowList, globalHeadersAllowList, globa
|
|
|
93
108
|
*/
|
|
94
109
|
var PingopsSpanProcessor = class {
|
|
95
110
|
processor;
|
|
111
|
+
exporterTraceUrl;
|
|
96
112
|
config;
|
|
97
113
|
/**
|
|
98
114
|
* Creates a new PingopsSpanProcessor instance.
|
|
@@ -102,8 +118,9 @@ var PingopsSpanProcessor = class {
|
|
|
102
118
|
constructor(config) {
|
|
103
119
|
const exportMode = config.exportMode ?? "batched";
|
|
104
120
|
const apiKey = config.apiKey || process.env.PINGOPS_API_KEY || "";
|
|
121
|
+
this.exporterTraceUrl = `${config.baseUrl}/v1/traces`;
|
|
105
122
|
const exporter = new _opentelemetry_exporter_trace_otlp_http.OTLPTraceExporter({
|
|
106
|
-
url:
|
|
123
|
+
url: this.exporterTraceUrl,
|
|
107
124
|
headers: {
|
|
108
125
|
Authorization: apiKey ? `Bearer ${apiKey}` : "",
|
|
109
126
|
"Content-Type": "application/json"
|
|
@@ -131,7 +148,7 @@ var PingopsSpanProcessor = class {
|
|
|
131
148
|
domainAllowList: config.domainAllowList,
|
|
132
149
|
maxRequestBodySize: config.maxRequestBodySize,
|
|
133
150
|
maxResponseBodySize: config.maxResponseBodySize,
|
|
134
|
-
exportTraceUrl:
|
|
151
|
+
exportTraceUrl: this.exporterTraceUrl
|
|
135
152
|
});
|
|
136
153
|
logger$2.info("Initialized PingopsSpanProcessor", {
|
|
137
154
|
baseUrl: config.baseUrl,
|
|
@@ -192,6 +209,14 @@ var PingopsSpanProcessor = class {
|
|
|
192
209
|
}
|
|
193
210
|
const attributes = span.attributes;
|
|
194
211
|
const url$1 = (0, _pingops_core.getHttpUrlFromAttributes)(attributes) ?? "";
|
|
212
|
+
if (url$1 && isExporterRequestUrl$1(url$1, this.exporterTraceUrl)) {
|
|
213
|
+
logger$2.debug("Skipping exporter span to prevent self-instrumentation", {
|
|
214
|
+
spanName: span.name,
|
|
215
|
+
spanId: spanContext.spanId,
|
|
216
|
+
url: url$1
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
195
220
|
logger$2.debug("Extracted URL for domain filtering", {
|
|
196
221
|
spanName: span.name,
|
|
197
222
|
url: url$1,
|
|
@@ -363,9 +388,12 @@ async function shutdownTracerProvider() {
|
|
|
363
388
|
//#region src/instrumentations/suppression-guard.ts
|
|
364
389
|
const logger = (0, _pingops_core.createLogger)("[PingOps SuppressionGuard]");
|
|
365
390
|
let hasLoggedSuppressionLeakWarning = false;
|
|
366
|
-
function
|
|
391
|
+
function normalizePath(pathname) {
|
|
392
|
+
return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
393
|
+
}
|
|
394
|
+
function parseUrl(url$1) {
|
|
367
395
|
try {
|
|
368
|
-
return new URL(url$1)
|
|
396
|
+
return new URL(url$1);
|
|
369
397
|
} catch {
|
|
370
398
|
return null;
|
|
371
399
|
}
|
|
@@ -374,10 +402,20 @@ function isExporterRequestUrl(requestUrl) {
|
|
|
374
402
|
if (!requestUrl) return false;
|
|
375
403
|
const exporterUrl = getGlobalConfig()?.exportTraceUrl;
|
|
376
404
|
if (!exporterUrl) return false;
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
-
if (!
|
|
380
|
-
|
|
405
|
+
const parsedRequestUrl = parseUrl(requestUrl);
|
|
406
|
+
const parsedExporterUrl = parseUrl(exporterUrl);
|
|
407
|
+
if (!parsedRequestUrl || !parsedExporterUrl) return false;
|
|
408
|
+
if (parsedRequestUrl.origin !== parsedExporterUrl.origin) return false;
|
|
409
|
+
const requestPath = normalizePath(parsedRequestUrl.pathname);
|
|
410
|
+
const exporterPath = normalizePath(parsedExporterUrl.pathname);
|
|
411
|
+
return requestPath === exporterPath || requestPath.startsWith(`${exporterPath}/`);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Determines whether an outbound request should be skipped from instrumentation
|
|
415
|
+
* to prevent exporter self-instrumentation loops.
|
|
416
|
+
*/
|
|
417
|
+
function shouldIgnoreOutboundInstrumentation(requestUrl) {
|
|
418
|
+
return isExporterRequestUrl(requestUrl);
|
|
381
419
|
}
|
|
382
420
|
/**
|
|
383
421
|
* Returns a context for outbound span creation that neutralizes leaked suppression
|
|
@@ -401,6 +439,7 @@ function resolveOutboundSpanParentContext(activeContext, requestUrl) {
|
|
|
401
439
|
*/
|
|
402
440
|
const DEFAULT_MAX_REQUEST_BODY_SIZE$1 = 4 * 1024;
|
|
403
441
|
const DEFAULT_MAX_RESPONSE_BODY_SIZE$1 = 4 * 1024;
|
|
442
|
+
const LEGACY_ATTR_HTTP_URL = "http.url";
|
|
404
443
|
const PingopsSemanticAttributes = {
|
|
405
444
|
HTTP_REQUEST_BODY: "http.request.body",
|
|
406
445
|
HTTP_RESPONSE_BODY: "http.response.body"
|
|
@@ -408,12 +447,18 @@ const PingopsSemanticAttributes = {
|
|
|
408
447
|
/**
|
|
409
448
|
* Manually flattens a nested object into dot-notation keys
|
|
410
449
|
*/
|
|
450
|
+
function isPlainObject(value) {
|
|
451
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Buffer);
|
|
452
|
+
}
|
|
453
|
+
function isPrimitiveArray(value) {
|
|
454
|
+
return value.every((item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean");
|
|
455
|
+
}
|
|
411
456
|
function flatten(obj, prefix = "") {
|
|
412
457
|
const result = {};
|
|
413
458
|
for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
414
459
|
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
415
460
|
const value = obj[key];
|
|
416
|
-
if (
|
|
461
|
+
if (isPlainObject(value)) Object.assign(result, flatten(value, newKey));
|
|
417
462
|
else result[newKey] = value;
|
|
418
463
|
}
|
|
419
464
|
return result;
|
|
@@ -424,11 +469,9 @@ function flatten(obj, prefix = "") {
|
|
|
424
469
|
function setAttributeValue(span, attrName, attrValue) {
|
|
425
470
|
if (typeof attrValue === "string" || typeof attrValue === "number" || typeof attrValue === "boolean") span.setAttribute(attrName, attrValue);
|
|
426
471
|
else if (attrValue instanceof Buffer) span.setAttribute(attrName, attrValue.toString("utf8"));
|
|
427
|
-
else if (
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (typeof firstElement === "string" || typeof firstElement === "number" || typeof firstElement === "boolean") span.setAttribute(attrName, attrValue);
|
|
431
|
-
} else span.setAttribute(attrName, attrValue);
|
|
472
|
+
else if (Array.isArray(attrValue)) {
|
|
473
|
+
if (attrValue.length === 0 || isPrimitiveArray(attrValue)) span.setAttribute(attrName, attrValue);
|
|
474
|
+
} else if (isPlainObject(attrValue)) span.setAttributes(flatten({ [attrName]: attrValue }));
|
|
432
475
|
}
|
|
433
476
|
/**
|
|
434
477
|
* Extracts domain from URL
|
|
@@ -494,9 +537,16 @@ function captureRequestBody(span, data, maxSize, semanticAttr, url$1) {
|
|
|
494
537
|
/**
|
|
495
538
|
* Captures response body from chunks
|
|
496
539
|
*/
|
|
497
|
-
function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url$1) {
|
|
540
|
+
function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url$1, maxSize) {
|
|
498
541
|
if (!shouldCaptureResponseBody$1(url$1)) return;
|
|
499
|
-
if (chunks
|
|
542
|
+
if (chunks === null) {
|
|
543
|
+
const contentEncoding = responseHeaders?.["content-encoding"];
|
|
544
|
+
const contentType = responseHeaders?.["content-type"];
|
|
545
|
+
const toHeaderString = (value) => typeof value === "string" ? value : Array.isArray(value) ? value.join(", ") : "unknown";
|
|
546
|
+
setAttributeValue(span, semanticAttr, `[truncated response body; exceeded maxResponseBodySize=${maxSize ?? DEFAULT_MAX_RESPONSE_BODY_SIZE$1}; content-type=${toHeaderString(contentType)}; content-encoding=${toHeaderString(contentEncoding)}]`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (chunks.length) try {
|
|
500
550
|
const concatedChunks = Buffer.concat(chunks);
|
|
501
551
|
const contentEncoding = responseHeaders?.["content-encoding"];
|
|
502
552
|
if ((0, _pingops_core.isCompressedContentEncoding)(contentEncoding)) {
|
|
@@ -559,7 +609,7 @@ function extractRequestUrlFromSpanOptions(options) {
|
|
|
559
609
|
const attrs = options.attributes;
|
|
560
610
|
if (!attrs) return;
|
|
561
611
|
if (typeof attrs[_opentelemetry_semantic_conventions.ATTR_URL_FULL] === "string") return attrs[_opentelemetry_semantic_conventions.ATTR_URL_FULL];
|
|
562
|
-
if (typeof attrs[
|
|
612
|
+
if (typeof attrs[LEGACY_ATTR_HTTP_URL] === "string") return attrs[LEGACY_ATTR_HTTP_URL];
|
|
563
613
|
const scheme = typeof attrs[_opentelemetry_semantic_conventions.ATTR_URL_SCHEME] === "string" ? attrs[_opentelemetry_semantic_conventions.ATTR_URL_SCHEME] : "http";
|
|
564
614
|
const host = typeof attrs[_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS] === "string" ? attrs[_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS] : void 0;
|
|
565
615
|
if (!host) return;
|
|
@@ -598,17 +648,19 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
598
648
|
if (headers) captureRequestHeaders(span, headers);
|
|
599
649
|
if (request instanceof http.ClientRequest) {
|
|
600
650
|
const maxRequestBodySize = config?.maxRequestBodySize || DEFAULT_MAX_REQUEST_BODY_SIZE$1;
|
|
601
|
-
const
|
|
651
|
+
const hostHeader = request.getHeader("host");
|
|
652
|
+
const host = typeof hostHeader === "string" ? hostHeader : Array.isArray(hostHeader) ? hostHeader.join(",") : typeof hostHeader === "number" ? String(hostHeader) : void 0;
|
|
653
|
+
const url$1 = request.path && host ? `${request.protocol || "http:"}//${host}${request.path}` : void 0;
|
|
602
654
|
const originalWrite = request.write.bind(request);
|
|
603
655
|
const originalEnd = request.end.bind(request);
|
|
604
|
-
request.write = (data) => {
|
|
656
|
+
request.write = ((data, ...rest) => {
|
|
605
657
|
if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url$1);
|
|
606
|
-
return originalWrite(data);
|
|
607
|
-
};
|
|
608
|
-
request.end = (data) => {
|
|
658
|
+
return originalWrite(data, ...rest);
|
|
659
|
+
});
|
|
660
|
+
request.end = ((data, ...rest) => {
|
|
609
661
|
if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url$1);
|
|
610
|
-
return originalEnd(data);
|
|
611
|
-
};
|
|
662
|
+
return originalEnd(data, ...rest);
|
|
663
|
+
});
|
|
612
664
|
}
|
|
613
665
|
if (originalRequestHook) originalRequestHook(span, request);
|
|
614
666
|
};
|
|
@@ -625,15 +677,24 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
625
677
|
const shouldCapture = shouldCaptureResponseBody$1(url$1);
|
|
626
678
|
response.prependListener("data", (chunk) => {
|
|
627
679
|
if (!chunk || !shouldCapture) return;
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
680
|
+
let chunkBuffer = null;
|
|
681
|
+
if (typeof chunk === "string") chunkBuffer = Buffer.from(chunk);
|
|
682
|
+
else if (Buffer.isBuffer(chunk)) chunkBuffer = chunk;
|
|
683
|
+
else if (chunk instanceof Uint8Array) chunkBuffer = Buffer.from(chunk);
|
|
684
|
+
if (!chunkBuffer) return;
|
|
685
|
+
totalSize += chunkBuffer.length;
|
|
686
|
+
if (chunks && totalSize <= maxResponseBodySize) chunks.push(chunkBuffer);
|
|
687
|
+
else chunks = null;
|
|
636
688
|
});
|
|
689
|
+
let finalized = false;
|
|
690
|
+
const finalizeCapture = () => {
|
|
691
|
+
if (finalized) return;
|
|
692
|
+
finalized = true;
|
|
693
|
+
captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, headers, url$1, maxResponseBodySize);
|
|
694
|
+
};
|
|
695
|
+
response.prependOnceListener("end", finalizeCapture);
|
|
696
|
+
response.prependOnceListener("close", finalizeCapture);
|
|
697
|
+
response.prependOnceListener("aborted", finalizeCapture);
|
|
637
698
|
}
|
|
638
699
|
if (originalResponseHook) originalResponseHook(span, response);
|
|
639
700
|
};
|
|
@@ -645,6 +706,14 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
645
706
|
/**
|
|
646
707
|
* HTTP instrumentation for OpenTelemetry
|
|
647
708
|
*/
|
|
709
|
+
function toRequestUrl$1(request) {
|
|
710
|
+
if (typeof request.href === "string" && request.href.length > 0) return request.href;
|
|
711
|
+
const protocol = typeof request.protocol === "string" && request.protocol.length > 0 ? request.protocol : "http:";
|
|
712
|
+
const hostnameOrHost = typeof request.hostname === "string" && request.hostname.length > 0 ? request.hostname : request.host;
|
|
713
|
+
if (!hostnameOrHost) return;
|
|
714
|
+
const hasPortInHost = hostnameOrHost.includes(":");
|
|
715
|
+
return `${protocol}//${hostnameOrHost}${request.port != null && !hasPortInHost ? `:${request.port}` : ""}${typeof request.path === "string" ? request.path : typeof request.pathname === "string" ? request.pathname : "/"}`;
|
|
716
|
+
}
|
|
648
717
|
/**
|
|
649
718
|
* Creates an HTTP instrumentation instance.
|
|
650
719
|
* All outgoing HTTP requests are instrumented when the SDK is initialized.
|
|
@@ -654,12 +723,16 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
654
723
|
*/
|
|
655
724
|
function createHttpInstrumentation(config) {
|
|
656
725
|
const globalConfig$1 = getGlobalConfig();
|
|
726
|
+
const userIgnoreOutgoingRequestHook = config?.ignoreOutgoingRequestHook;
|
|
657
727
|
return new PingopsHttpInstrumentation({
|
|
728
|
+
...config,
|
|
658
729
|
ignoreIncomingRequestHook: () => true,
|
|
659
|
-
ignoreOutgoingRequestHook: () =>
|
|
730
|
+
ignoreOutgoingRequestHook: (request) => {
|
|
731
|
+
if (shouldIgnoreOutboundInstrumentation(toRequestUrl$1(request))) return true;
|
|
732
|
+
return userIgnoreOutgoingRequestHook?.(request) ?? false;
|
|
733
|
+
},
|
|
660
734
|
maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
|
|
661
|
-
maxResponseBodySize: globalConfig$1?.maxResponseBodySize
|
|
662
|
-
...config
|
|
735
|
+
maxResponseBodySize: globalConfig$1?.maxResponseBodySize
|
|
663
736
|
});
|
|
664
737
|
}
|
|
665
738
|
|
|
@@ -875,11 +948,11 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
875
948
|
const record = this._recordFromReq.get(request);
|
|
876
949
|
if (!record) return;
|
|
877
950
|
const { span } = record;
|
|
878
|
-
const
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
951
|
+
const spanAttributes = {};
|
|
952
|
+
const remoteAddress = typeof socket.remoteAddress === "string" ? socket.remoteAddress : void 0;
|
|
953
|
+
const remotePort = typeof socket.remotePort === "number" ? socket.remotePort : void 0;
|
|
954
|
+
if (remoteAddress) spanAttributes[_opentelemetry_semantic_conventions.ATTR_NETWORK_PEER_ADDRESS] = remoteAddress;
|
|
955
|
+
if (remotePort !== void 0) spanAttributes[_opentelemetry_semantic_conventions.ATTR_NETWORK_PEER_PORT] = remotePort;
|
|
883
956
|
const headersMap = this.parseRequestHeaders(request);
|
|
884
957
|
for (const [name, value] of headersMap.entries()) {
|
|
885
958
|
const attrValue = Array.isArray(value) ? value.join(", ") : value;
|
|
@@ -948,14 +1021,15 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
948
1021
|
this._diag.error("Error occurred while capturing request body:", e);
|
|
949
1022
|
}
|
|
950
1023
|
}
|
|
1024
|
+
const errorMessage = error.message;
|
|
951
1025
|
span.recordException(error);
|
|
952
1026
|
span.setStatus({
|
|
953
1027
|
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
954
|
-
message:
|
|
1028
|
+
message: errorMessage
|
|
955
1029
|
});
|
|
956
1030
|
span.end();
|
|
957
1031
|
this._recordFromReq.delete(request);
|
|
958
|
-
attributes[_opentelemetry_semantic_conventions.ATTR_ERROR_TYPE] =
|
|
1032
|
+
attributes[_opentelemetry_semantic_conventions.ATTR_ERROR_TYPE] = errorMessage;
|
|
959
1033
|
this.recordRequestDuration(attributes, startTime);
|
|
960
1034
|
}
|
|
961
1035
|
onBodyChunkSent({ request, chunk }) {
|
|
@@ -1038,6 +1112,13 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
1038
1112
|
/**
|
|
1039
1113
|
* Undici instrumentation for OpenTelemetry
|
|
1040
1114
|
*/
|
|
1115
|
+
function toRequestUrl(request) {
|
|
1116
|
+
try {
|
|
1117
|
+
return new URL(request.path, request.origin).toString();
|
|
1118
|
+
} catch {
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1041
1122
|
/**
|
|
1042
1123
|
* Creates an Undici instrumentation instance.
|
|
1043
1124
|
* All requests are instrumented when the SDK is initialized.
|
|
@@ -1048,7 +1129,9 @@ function createUndiciInstrumentation() {
|
|
|
1048
1129
|
const globalConfig$1 = getGlobalConfig();
|
|
1049
1130
|
return new UndiciInstrumentation({
|
|
1050
1131
|
enabled: true,
|
|
1051
|
-
ignoreRequestHook: () =>
|
|
1132
|
+
ignoreRequestHook: (request) => {
|
|
1133
|
+
return shouldIgnoreOutboundInstrumentation(toRequestUrl(request));
|
|
1134
|
+
},
|
|
1052
1135
|
maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
|
|
1053
1136
|
maxResponseBodySize: globalConfig$1?.maxResponseBodySize
|
|
1054
1137
|
});
|