@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 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 and network timing metrics
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 responseBody = Buffer.concat(chunks).toString("utf8");
494
- if (responseBody) setAttributeValue(span, semanticAttr, responseBody);
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(`pingops.http.request.header.${key.toLowerCase()}`, Array.isArray(value) ? value.join(",") : String(value));
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(`pingops.http.response.header.${key.toLowerCase()}`, Array.isArray(value) ? value.join(",") : String(value));
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.headers;
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.headers;
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
- if (networkTimings) {
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(isGlobalInstrumentationEnabled, config) {
596
+ function createHttpInstrumentation(config) {
597
+ const globalConfig$1 = getGlobalConfig();
605
598
  return new PingopsHttpInstrumentation({
606
599
  ignoreIncomingRequestHook: () => true,
607
- ignoreOutgoingRequestHook: () => {
608
- if (isGlobalInstrumentationEnabled()) return false;
609
- return _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_HTTP_ENABLED) !== true;
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
- if (record.responseBodyChunks.length > 0 && record.responseBodySize !== Infinity) try {
868
- const responseBody = Buffer.concat(record.responseBodyChunks).toString("utf-8");
869
- if (responseBody) span.setAttribute(HTTP_RESPONSE_BODY, responseBody);
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 if (record.requestBodyChunks.length === 0) record.requestBodySize = Infinity;
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.requestBodyChunks.length > 0 && record.requestBodySize !== Infinity) try {
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 if (record.responseBodyChunks.length === 0) record.responseBodySize = Infinity;
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(isGlobalInstrumentationEnabled) {
988
+ function createUndiciInstrumentation() {
989
+ const globalConfig$1 = getGlobalConfig();
978
990
  return new UndiciInstrumentation({
979
991
  enabled: true,
980
- ignoreRequestHook: () => {
981
- if (isGlobalInstrumentationEnabled()) return false;
982
- return _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_HTTP_ENABLED) !== true;
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(isGlobalInstrumentationEnabled) {
1008
+ function getInstrumentations() {
1002
1009
  if (installed) return [];
1003
1010
  installed = true;
1004
- return [createHttpInstrumentation(isGlobalInstrumentationEnabled), createUndiciInstrumentation(isGlobalInstrumentationEnabled)];
1011
+ return [createHttpInstrumentation(), createUndiciInstrumentation()];
1005
1012
  }
1006
1013
 
1007
1014
  //#endregion