@pingops/otel 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +95 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -31
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +29 -31
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +96 -89
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -128,7 +128,9 @@ var PingopsSpanProcessor = class {
|
|
|
128
128
|
setGlobalConfig({
|
|
129
129
|
captureRequestBody: config.captureRequestBody,
|
|
130
130
|
captureResponseBody: config.captureResponseBody,
|
|
131
|
-
domainAllowList: config.domainAllowList
|
|
131
|
+
domainAllowList: config.domainAllowList,
|
|
132
|
+
maxRequestBodySize: config.maxRequestBodySize,
|
|
133
|
+
maxResponseBodySize: config.maxResponseBodySize
|
|
132
134
|
});
|
|
133
135
|
logger$1.info("Initialized PingopsSpanProcessor", {
|
|
134
136
|
baseUrl: config.baseUrl,
|
|
@@ -360,19 +362,13 @@ async function shutdownTracerProvider() {
|
|
|
360
362
|
//#region src/instrumentations/http/pingops-http.ts
|
|
361
363
|
/**
|
|
362
364
|
* Pingops HTTP instrumentation that extends HttpInstrumentation
|
|
363
|
-
* with request/response body capture
|
|
365
|
+
* with request/response body capture
|
|
364
366
|
*/
|
|
365
367
|
const DEFAULT_MAX_REQUEST_BODY_SIZE$1 = 4 * 1024;
|
|
366
368
|
const DEFAULT_MAX_RESPONSE_BODY_SIZE$1 = 4 * 1024;
|
|
367
|
-
const NETWORK_TIMINGS_PROP_NAME = "__networkTimings";
|
|
368
369
|
const PingopsSemanticAttributes = {
|
|
369
370
|
HTTP_REQUEST_BODY: "http.request.body",
|
|
370
|
-
HTTP_RESPONSE_BODY: "http.response.body"
|
|
371
|
-
NETWORK_DNS_LOOKUP_DURATION: "net.dns.lookup.duration",
|
|
372
|
-
NETWORK_TCP_CONNECT_DURATION: "net.tcp.connect.duration",
|
|
373
|
-
NETWORK_TLS_HANDSHAKE_DURATION: "net.tls.handshake.duration",
|
|
374
|
-
NETWORK_TTFB_DURATION: "net.ttfb.duration",
|
|
375
|
-
NETWORK_CONTENT_TRANSFER_DURATION: "net.content.transfer.duration"
|
|
371
|
+
HTTP_RESPONSE_BODY: "http.response.body"
|
|
376
372
|
};
|
|
377
373
|
/**
|
|
378
374
|
* Manually flattens a nested object into dot-notation keys
|
|
@@ -400,30 +396,6 @@ function setAttributeValue(span, attrName, attrValue) {
|
|
|
400
396
|
} else span.setAttribute(attrName, attrValue);
|
|
401
397
|
}
|
|
402
398
|
/**
|
|
403
|
-
* Processes network timings and sets them as span attributes (no spans created)
|
|
404
|
-
*/
|
|
405
|
-
function processNetworkTimings(span, networkTimings) {
|
|
406
|
-
if (networkTimings.startAt && networkTimings.dnsLookupAt) span.setAttribute(PingopsSemanticAttributes.NETWORK_DNS_LOOKUP_DURATION, networkTimings.dnsLookupAt - networkTimings.startAt);
|
|
407
|
-
if (networkTimings.dnsLookupAt && networkTimings.tcpConnectionAt) span.setAttribute(PingopsSemanticAttributes.NETWORK_TCP_CONNECT_DURATION, networkTimings.tcpConnectionAt - networkTimings.dnsLookupAt);
|
|
408
|
-
if (networkTimings.tcpConnectionAt && networkTimings.tlsHandshakeAt) span.setAttribute(PingopsSemanticAttributes.NETWORK_TLS_HANDSHAKE_DURATION, networkTimings.tlsHandshakeAt - networkTimings.tcpConnectionAt);
|
|
409
|
-
const startTTFB = networkTimings.tlsHandshakeAt || networkTimings.tcpConnectionAt;
|
|
410
|
-
if (networkTimings.firstByteAt && startTTFB) span.setAttribute(PingopsSemanticAttributes.NETWORK_TTFB_DURATION, networkTimings.firstByteAt - startTTFB);
|
|
411
|
-
if (networkTimings.firstByteAt && networkTimings.endAt) span.setAttribute(PingopsSemanticAttributes.NETWORK_CONTENT_TRANSFER_DURATION, networkTimings.endAt - networkTimings.firstByteAt);
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Initializes network timings on a span
|
|
415
|
-
*/
|
|
416
|
-
function initializeNetworkTimings(span) {
|
|
417
|
-
const networkTimings = { startAt: Date.now() };
|
|
418
|
-
Object.defineProperty(span, NETWORK_TIMINGS_PROP_NAME, {
|
|
419
|
-
enumerable: false,
|
|
420
|
-
configurable: true,
|
|
421
|
-
writable: false,
|
|
422
|
-
value: networkTimings
|
|
423
|
-
});
|
|
424
|
-
return networkTimings;
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
399
|
* Extracts domain from URL
|
|
428
400
|
*/
|
|
429
401
|
function extractDomainFromUrl$1(url$1) {
|
|
@@ -487,26 +459,66 @@ function captureRequestBody(span, data, maxSize, semanticAttr, url$1) {
|
|
|
487
459
|
/**
|
|
488
460
|
* Captures response body from chunks
|
|
489
461
|
*/
|
|
490
|
-
function captureResponseBody(span, chunks, semanticAttr, url$1) {
|
|
462
|
+
function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url$1) {
|
|
491
463
|
if (!shouldCaptureResponseBody$1(url$1)) return;
|
|
492
464
|
if (chunks && chunks.length) try {
|
|
493
|
-
const
|
|
494
|
-
|
|
465
|
+
const concatedChunks = Buffer.concat(chunks);
|
|
466
|
+
const contentEncoding = responseHeaders?.["content-encoding"];
|
|
467
|
+
if ((0, _pingops_core.isCompressedContentEncoding)(contentEncoding)) {
|
|
468
|
+
setAttributeValue(span, semanticAttr, concatedChunks.toString("base64"));
|
|
469
|
+
const encStr = typeof contentEncoding === "string" ? contentEncoding : Array.isArray(contentEncoding) ? contentEncoding.map(String).join(", ") : void 0;
|
|
470
|
+
if (encStr) setAttributeValue(span, _pingops_core.HTTP_RESPONSE_CONTENT_ENCODING, encStr);
|
|
471
|
+
} else {
|
|
472
|
+
const bodyStr = (0, _pingops_core.bufferToBodyString)(concatedChunks);
|
|
473
|
+
if (bodyStr != null) setAttributeValue(span, semanticAttr, bodyStr);
|
|
474
|
+
}
|
|
495
475
|
} catch (e) {
|
|
496
476
|
console.error("Error occurred while capturing response body:", e);
|
|
497
477
|
}
|
|
498
478
|
}
|
|
499
479
|
/**
|
|
480
|
+
* Extracts headers from a request object (ClientRequest or IncomingMessage)
|
|
481
|
+
* Handles both types efficiently by checking for available methods/properties
|
|
482
|
+
*/
|
|
483
|
+
function extractRequestHeaders(request) {
|
|
484
|
+
if ("headers" in request && request.headers) return request.headers;
|
|
485
|
+
if (typeof request.getHeaders === "function") try {
|
|
486
|
+
const headers = request.getHeaders();
|
|
487
|
+
const result = {};
|
|
488
|
+
for (const [key, value] of Object.entries(headers)) if (value !== void 0) result[key] = typeof value === "number" ? String(value) : Array.isArray(value) ? value.map(String) : String(value);
|
|
489
|
+
return result;
|
|
490
|
+
} catch {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Extracts headers from a response object (ServerResponse or IncomingMessage)
|
|
497
|
+
* Handles both types efficiently by checking for available methods/properties
|
|
498
|
+
*/
|
|
499
|
+
function extractResponseHeaders(response) {
|
|
500
|
+
if ("headers" in response && response.headers) return response.headers;
|
|
501
|
+
if (typeof response.getHeaders === "function") try {
|
|
502
|
+
const headers = response.getHeaders();
|
|
503
|
+
const result = {};
|
|
504
|
+
for (const [key, value] of Object.entries(headers)) if (value !== void 0) result[key] = typeof value === "number" ? String(value) : Array.isArray(value) ? value.map(String) : String(value);
|
|
505
|
+
return result;
|
|
506
|
+
} catch {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
500
512
|
* Captures HTTP request headers as span attributes
|
|
501
513
|
*/
|
|
502
514
|
function captureRequestHeaders(span, headers) {
|
|
503
|
-
for (const [key, value] of Object.entries(headers)) if (value !== void 0) span.setAttribute(`
|
|
515
|
+
for (const [key, value] of Object.entries(headers)) if (value !== void 0) span.setAttribute(`http.request.header.${key.toLowerCase()}`, Array.isArray(value) ? value.join(",") : String(value));
|
|
504
516
|
}
|
|
505
517
|
/**
|
|
506
518
|
* Captures HTTP response headers as span attributes
|
|
507
519
|
*/
|
|
508
520
|
function captureResponseHeaders(span, headers) {
|
|
509
|
-
for (const [key, value] of Object.entries(headers)) if (value !== void 0) span.setAttribute(`
|
|
521
|
+
for (const [key, value] of Object.entries(headers)) if (value !== void 0) span.setAttribute(`http.response.header.${key.toLowerCase()}`, Array.isArray(value) ? value.join(",") : String(value));
|
|
510
522
|
}
|
|
511
523
|
const PingopsHttpSemanticAttributes = PingopsSemanticAttributes;
|
|
512
524
|
var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_http.HttpInstrumentation {
|
|
@@ -523,10 +535,9 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
523
535
|
}
|
|
524
536
|
_createRequestHook(originalRequestHook, config) {
|
|
525
537
|
return (span, request) => {
|
|
526
|
-
const headers = request
|
|
538
|
+
const headers = extractRequestHeaders(request);
|
|
527
539
|
if (headers) captureRequestHeaders(span, headers);
|
|
528
540
|
if (request instanceof http.ClientRequest) {
|
|
529
|
-
const networkTimings = initializeNetworkTimings(span);
|
|
530
541
|
const maxRequestBodySize = config?.maxRequestBodySize || DEFAULT_MAX_REQUEST_BODY_SIZE$1;
|
|
531
542
|
const url$1 = request.path && request.getHeader("host") ? `${request.protocol || "http:"}//${request.getHeader("host")}${request.path}` : void 0;
|
|
532
543
|
const originalWrite = request.write.bind(request);
|
|
@@ -539,27 +550,15 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
539
550
|
if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url$1);
|
|
540
551
|
return originalEnd(data);
|
|
541
552
|
};
|
|
542
|
-
request.on("socket", (socket) => {
|
|
543
|
-
socket.on("lookup", () => {
|
|
544
|
-
networkTimings.dnsLookupAt = Date.now();
|
|
545
|
-
});
|
|
546
|
-
socket.on("connect", () => {
|
|
547
|
-
networkTimings.tcpConnectionAt = Date.now();
|
|
548
|
-
});
|
|
549
|
-
socket.on("secureConnect", () => {
|
|
550
|
-
networkTimings.tlsHandshakeAt = Date.now();
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
553
|
}
|
|
554
554
|
if (originalRequestHook) originalRequestHook(span, request);
|
|
555
555
|
};
|
|
556
556
|
}
|
|
557
557
|
_createResponseHook(originalResponseHook, config) {
|
|
558
558
|
return (span, response) => {
|
|
559
|
-
const headers = response
|
|
559
|
+
const headers = extractResponseHeaders(response);
|
|
560
560
|
if (headers) captureResponseHeaders(span, headers);
|
|
561
561
|
if (response instanceof http.IncomingMessage) {
|
|
562
|
-
const networkTimings = span[NETWORK_TIMINGS_PROP_NAME];
|
|
563
562
|
const maxResponseBodySize = config?.maxResponseBodySize || DEFAULT_MAX_RESPONSE_BODY_SIZE$1;
|
|
564
563
|
const url$1 = response.url || void 0;
|
|
565
564
|
let chunks = [];
|
|
@@ -574,14 +573,7 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
574
573
|
}
|
|
575
574
|
});
|
|
576
575
|
response.prependOnceListener("end", () => {
|
|
577
|
-
|
|
578
|
-
networkTimings.endAt = Date.now();
|
|
579
|
-
processNetworkTimings(span, networkTimings);
|
|
580
|
-
}
|
|
581
|
-
captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, url$1);
|
|
582
|
-
});
|
|
583
|
-
if (networkTimings) response.once("readable", () => {
|
|
584
|
-
networkTimings.firstByteAt = Date.now();
|
|
576
|
+
captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, headers, url$1);
|
|
585
577
|
});
|
|
586
578
|
}
|
|
587
579
|
if (originalResponseHook) originalResponseHook(span, response);
|
|
@@ -595,19 +587,19 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
595
587
|
* HTTP instrumentation for OpenTelemetry
|
|
596
588
|
*/
|
|
597
589
|
/**
|
|
598
|
-
* Creates an HTTP instrumentation instance
|
|
590
|
+
* Creates an HTTP instrumentation instance.
|
|
591
|
+
* All outgoing HTTP requests are instrumented when the SDK is initialized.
|
|
599
592
|
*
|
|
600
|
-
* @param isGlobalInstrumentationEnabled - Function that checks if global instrumentation is enabled
|
|
601
593
|
* @param config - Optional configuration for the instrumentation
|
|
602
594
|
* @returns PingopsHttpInstrumentation instance
|
|
603
595
|
*/
|
|
604
|
-
function createHttpInstrumentation(
|
|
596
|
+
function createHttpInstrumentation(config) {
|
|
597
|
+
const globalConfig$1 = getGlobalConfig();
|
|
605
598
|
return new PingopsHttpInstrumentation({
|
|
606
599
|
ignoreIncomingRequestHook: () => true,
|
|
607
|
-
ignoreOutgoingRequestHook: () =>
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
},
|
|
600
|
+
ignoreOutgoingRequestHook: () => false,
|
|
601
|
+
maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
|
|
602
|
+
maxResponseBodySize: globalConfig$1?.maxResponseBodySize,
|
|
611
603
|
...config
|
|
612
604
|
});
|
|
613
605
|
}
|
|
@@ -864,9 +856,19 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
864
856
|
if (!record) return;
|
|
865
857
|
const { span, attributes, startTime } = record;
|
|
866
858
|
if (shouldCaptureResponseBody(record.url)) {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
859
|
+
const maxResponseBodySize = this.getConfig().maxResponseBodySize ?? DEFAULT_MAX_RESPONSE_BODY_SIZE;
|
|
860
|
+
const contentEncoding = record.attributes?.["http.response.header.content-encoding"] ?? void 0;
|
|
861
|
+
const contentType = record.attributes?.["http.response.header.content-type"] ?? void 0;
|
|
862
|
+
if (record.responseBodySize === Infinity) span.setAttribute(HTTP_RESPONSE_BODY, `[truncated response body; exceeded maxResponseBodySize=${maxResponseBodySize}; content-type=${contentType ?? "unknown"}; content-encoding=${contentEncoding ?? "identity"}]`);
|
|
863
|
+
else if (record.responseBodyChunks.length > 0) try {
|
|
864
|
+
const responseBodyBuffer = Buffer.concat(record.responseBodyChunks);
|
|
865
|
+
if ((0, _pingops_core.isCompressedContentEncoding)(contentEncoding)) {
|
|
866
|
+
span.setAttribute(HTTP_RESPONSE_BODY, responseBodyBuffer.toString("base64"));
|
|
867
|
+
if (contentEncoding) span.setAttribute(_pingops_core.HTTP_RESPONSE_CONTENT_ENCODING, contentEncoding);
|
|
868
|
+
} else {
|
|
869
|
+
const bodyStr = (0, _pingops_core.bufferToBodyString)(responseBodyBuffer);
|
|
870
|
+
if (bodyStr != null) span.setAttribute(HTTP_RESPONSE_BODY, bodyStr);
|
|
871
|
+
}
|
|
870
872
|
} catch (e) {
|
|
871
873
|
this._diag.error("Error occurred while capturing response body:", e);
|
|
872
874
|
}
|
|
@@ -905,7 +907,10 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
905
907
|
if (record.requestBodySize + chunk.length <= maxRequestBodySize) {
|
|
906
908
|
record.requestBodyChunks.push(chunk);
|
|
907
909
|
record.requestBodySize += chunk.length;
|
|
908
|
-
} else
|
|
910
|
+
} else {
|
|
911
|
+
record.requestBodySize = Infinity;
|
|
912
|
+
record.requestBodyChunks = [];
|
|
913
|
+
}
|
|
909
914
|
}
|
|
910
915
|
onBodySent({ request }) {
|
|
911
916
|
const record = this._recordFromReq.get(request);
|
|
@@ -914,7 +919,10 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
914
919
|
record.requestBodyChunks = [];
|
|
915
920
|
return;
|
|
916
921
|
}
|
|
917
|
-
if (record.
|
|
922
|
+
if (record.requestBodySize === Infinity) {
|
|
923
|
+
const maxRequestBodySize = this.getConfig().maxRequestBodySize ?? DEFAULT_MAX_REQUEST_BODY_SIZE;
|
|
924
|
+
record.span.setAttribute(HTTP_REQUEST_BODY, `[truncated request body; exceeded maxRequestBodySize=${maxRequestBodySize}]`);
|
|
925
|
+
} else if (record.requestBodyChunks.length > 0) try {
|
|
918
926
|
const requestBody = Buffer.concat(record.requestBodyChunks).toString("utf-8");
|
|
919
927
|
if (requestBody) record.span.setAttribute(HTTP_REQUEST_BODY, requestBody);
|
|
920
928
|
} catch (e) {
|
|
@@ -930,7 +938,10 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
930
938
|
if (record.responseBodySize + chunk.length <= maxResponseBodySize) {
|
|
931
939
|
record.responseBodyChunks.push(chunk);
|
|
932
940
|
record.responseBodySize += chunk.length;
|
|
933
|
-
} else
|
|
941
|
+
} else {
|
|
942
|
+
record.responseBodySize = Infinity;
|
|
943
|
+
record.responseBodyChunks = [];
|
|
944
|
+
}
|
|
934
945
|
}
|
|
935
946
|
recordRequestDuration(attributes, startTime) {
|
|
936
947
|
const metricsAttributes = {};
|
|
@@ -969,18 +980,18 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
969
980
|
* Undici instrumentation for OpenTelemetry
|
|
970
981
|
*/
|
|
971
982
|
/**
|
|
972
|
-
* Creates an Undici instrumentation instance
|
|
983
|
+
* Creates an Undici instrumentation instance.
|
|
984
|
+
* All requests are instrumented when the SDK is initialized.
|
|
973
985
|
*
|
|
974
|
-
* @param isGlobalInstrumentationEnabled - Function that checks if global instrumentation is enabled
|
|
975
986
|
* @returns UndiciInstrumentation instance
|
|
976
987
|
*/
|
|
977
|
-
function createUndiciInstrumentation(
|
|
988
|
+
function createUndiciInstrumentation() {
|
|
989
|
+
const globalConfig$1 = getGlobalConfig();
|
|
978
990
|
return new UndiciInstrumentation({
|
|
979
991
|
enabled: true,
|
|
980
|
-
ignoreRequestHook: () =>
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
}
|
|
992
|
+
ignoreRequestHook: () => false,
|
|
993
|
+
maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
|
|
994
|
+
maxResponseBodySize: globalConfig$1?.maxResponseBodySize
|
|
984
995
|
});
|
|
985
996
|
}
|
|
986
997
|
|
|
@@ -990,18 +1001,14 @@ let installed = false;
|
|
|
990
1001
|
/**
|
|
991
1002
|
* Registers instrumentations for Node.js environment.
|
|
992
1003
|
* This function is idempotent and can be called multiple times safely.
|
|
1004
|
+
* When the SDK is initialized, all HTTP requests are instrumented.
|
|
993
1005
|
*
|
|
994
|
-
* Instrumentation behavior:
|
|
995
|
-
* - If global instrumentation is enabled: all HTTP requests are instrumented
|
|
996
|
-
* - If global instrumentation is NOT enabled: only requests within wrapHttp blocks are instrumented
|
|
997
|
-
*
|
|
998
|
-
* @param isGlobalInstrumentationEnabled - Function that checks if global instrumentation is enabled
|
|
999
1006
|
* @returns Array of Instrumentation instances
|
|
1000
1007
|
*/
|
|
1001
|
-
function getInstrumentations(
|
|
1008
|
+
function getInstrumentations() {
|
|
1002
1009
|
if (installed) return [];
|
|
1003
1010
|
installed = true;
|
|
1004
|
-
return [createHttpInstrumentation(
|
|
1011
|
+
return [createHttpInstrumentation(), createUndiciInstrumentation()];
|
|
1005
1012
|
}
|
|
1006
1013
|
|
|
1007
1014
|
//#endregion
|