@pingops/otel 0.2.4 → 0.2.6

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
@@ -439,6 +439,7 @@ function resolveOutboundSpanParentContext(activeContext, requestUrl) {
439
439
  */
440
440
  const DEFAULT_MAX_REQUEST_BODY_SIZE$1 = 4 * 1024;
441
441
  const DEFAULT_MAX_RESPONSE_BODY_SIZE$1 = 4 * 1024;
442
+ const LEGACY_ATTR_HTTP_URL = "http.url";
442
443
  const PingopsSemanticAttributes = {
443
444
  HTTP_REQUEST_BODY: "http.request.body",
444
445
  HTTP_RESPONSE_BODY: "http.response.body"
@@ -446,12 +447,18 @@ const PingopsSemanticAttributes = {
446
447
  /**
447
448
  * Manually flattens a nested object into dot-notation keys
448
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
+ }
449
456
  function flatten(obj, prefix = "") {
450
457
  const result = {};
451
458
  for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) {
452
459
  const newKey = prefix ? `${prefix}.${key}` : key;
453
460
  const value = obj[key];
454
- if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Buffer)) Object.assign(result, flatten(value, newKey));
461
+ if (isPlainObject(value)) Object.assign(result, flatten(value, newKey));
455
462
  else result[newKey] = value;
456
463
  }
457
464
  return result;
@@ -462,11 +469,9 @@ function flatten(obj, prefix = "") {
462
469
  function setAttributeValue(span, attrName, attrValue) {
463
470
  if (typeof attrValue === "string" || typeof attrValue === "number" || typeof attrValue === "boolean") span.setAttribute(attrName, attrValue);
464
471
  else if (attrValue instanceof Buffer) span.setAttribute(attrName, attrValue.toString("utf8"));
465
- else if (typeof attrValue == "object") span.setAttributes(flatten({ [attrName]: attrValue }));
466
- else if (Array.isArray(attrValue)) if (attrValue.length) {
467
- const firstElement = attrValue[0];
468
- if (typeof firstElement === "string" || typeof firstElement === "number" || typeof firstElement === "boolean") span.setAttribute(attrName, attrValue);
469
- } 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 }));
470
475
  }
471
476
  /**
472
477
  * Extracts domain from URL
@@ -532,9 +537,16 @@ function captureRequestBody(span, data, maxSize, semanticAttr, url$1) {
532
537
  /**
533
538
  * Captures response body from chunks
534
539
  */
535
- function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url$1) {
540
+ function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url$1, maxSize) {
536
541
  if (!shouldCaptureResponseBody$1(url$1)) return;
537
- if (chunks && chunks.length) try {
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 {
538
550
  const concatedChunks = Buffer.concat(chunks);
539
551
  const contentEncoding = responseHeaders?.["content-encoding"];
540
552
  if ((0, _pingops_core.isCompressedContentEncoding)(contentEncoding)) {
@@ -597,12 +609,27 @@ function extractRequestUrlFromSpanOptions(options) {
597
609
  const attrs = options.attributes;
598
610
  if (!attrs) return;
599
611
  if (typeof attrs[_opentelemetry_semantic_conventions.ATTR_URL_FULL] === "string") return attrs[_opentelemetry_semantic_conventions.ATTR_URL_FULL];
600
- if (typeof attrs[_opentelemetry_semantic_conventions.ATTR_HTTP_URL] === "string") return attrs[_opentelemetry_semantic_conventions.ATTR_HTTP_URL];
612
+ if (typeof attrs[LEGACY_ATTR_HTTP_URL] === "string") return attrs[LEGACY_ATTR_HTTP_URL];
601
613
  const scheme = typeof attrs[_opentelemetry_semantic_conventions.ATTR_URL_SCHEME] === "string" ? attrs[_opentelemetry_semantic_conventions.ATTR_URL_SCHEME] : "http";
602
614
  const host = typeof attrs[_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS] === "string" ? attrs[_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS] : void 0;
603
615
  if (!host) return;
604
616
  return `${scheme}://${host}${typeof attrs[_opentelemetry_semantic_conventions.ATTR_SERVER_PORT] === "number" ? `:${attrs[_opentelemetry_semantic_conventions.ATTR_SERVER_PORT]}` : ""}${typeof attrs[_opentelemetry_semantic_conventions.ATTR_URL_PATH] === "string" ? attrs[_opentelemetry_semantic_conventions.ATTR_URL_PATH] : "/"}${typeof attrs[_opentelemetry_semantic_conventions.ATTR_URL_QUERY] === "string" && attrs[_opentelemetry_semantic_conventions.ATTR_URL_QUERY].length > 0 ? `?${attrs[_opentelemetry_semantic_conventions.ATTR_URL_QUERY]}` : ""}`;
605
617
  }
618
+ function parseRequestPathAndQuery(pathWithQuery) {
619
+ const queryIndex = pathWithQuery.indexOf("?");
620
+ if (queryIndex < 0) return { path: pathWithQuery || "/" };
621
+ const path = pathWithQuery.slice(0, queryIndex) || "/";
622
+ const queryPart = pathWithQuery.slice(queryIndex);
623
+ return {
624
+ path,
625
+ query: queryPart.length > 0 ? queryPart : void 0
626
+ };
627
+ }
628
+ function extractClientRequestPath(request) {
629
+ if (!request || typeof request !== "object" || !("path" in request)) return;
630
+ const path = request.path;
631
+ return typeof path === "string" && path.length > 0 ? path : void 0;
632
+ }
606
633
  const PingopsHttpSemanticAttributes = PingopsSemanticAttributes;
607
634
  var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_http.HttpInstrumentation {
608
635
  constructor(config) {
@@ -636,17 +663,25 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
636
663
  if (headers) captureRequestHeaders(span, headers);
637
664
  if (request instanceof http.ClientRequest) {
638
665
  const maxRequestBodySize = config?.maxRequestBodySize || DEFAULT_MAX_REQUEST_BODY_SIZE$1;
639
- const url$1 = request.path && request.getHeader("host") ? `${request.protocol || "http:"}//${request.getHeader("host")}${request.path}` : void 0;
666
+ const hostHeader = request.getHeader("host");
667
+ const host = typeof hostHeader === "string" ? hostHeader : Array.isArray(hostHeader) ? hostHeader.join(",") : typeof hostHeader === "number" ? String(hostHeader) : void 0;
668
+ const url$1 = request.path && host ? `${request.protocol || "http:"}//${host}${request.path}` : void 0;
669
+ if (typeof request.path === "string" && request.path.length > 0) {
670
+ const { path, query } = parseRequestPathAndQuery(request.path);
671
+ span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_PATH, path);
672
+ if (query) span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_QUERY, query);
673
+ }
674
+ if (url$1) span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_FULL, url$1);
640
675
  const originalWrite = request.write.bind(request);
641
676
  const originalEnd = request.end.bind(request);
642
- request.write = (data) => {
677
+ request.write = ((data, ...rest) => {
643
678
  if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url$1);
644
- return originalWrite(data);
645
- };
646
- request.end = (data) => {
679
+ return originalWrite(data, ...rest);
680
+ });
681
+ request.end = ((data, ...rest) => {
647
682
  if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url$1);
648
- return originalEnd(data);
649
- };
683
+ return originalEnd(data, ...rest);
684
+ });
650
685
  }
651
686
  if (originalRequestHook) originalRequestHook(span, request);
652
687
  };
@@ -656,6 +691,12 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
656
691
  const headers = extractResponseHeaders(response);
657
692
  if (headers) captureResponseHeaders(span, headers);
658
693
  if (response instanceof http.IncomingMessage) {
694
+ const requestPath = response.req instanceof http.ClientRequest ? extractClientRequestPath(response.req) : void 0;
695
+ if (requestPath) {
696
+ const { path, query } = parseRequestPathAndQuery(requestPath);
697
+ span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_PATH, path);
698
+ if (query) span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_QUERY, query);
699
+ }
659
700
  const maxResponseBodySize = config?.maxResponseBodySize || DEFAULT_MAX_RESPONSE_BODY_SIZE$1;
660
701
  const url$1 = response.url || void 0;
661
702
  let chunks = [];
@@ -663,15 +704,24 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
663
704
  const shouldCapture = shouldCaptureResponseBody$1(url$1);
664
705
  response.prependListener("data", (chunk) => {
665
706
  if (!chunk || !shouldCapture) return;
666
- if (typeof chunk === "string" || chunk instanceof Buffer) {
667
- totalSize += chunk.length;
668
- if (chunks && totalSize <= maxResponseBodySize) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
669
- else chunks = null;
670
- }
671
- });
672
- response.prependOnceListener("end", () => {
673
- captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, headers, url$1);
707
+ let chunkBuffer = null;
708
+ if (typeof chunk === "string") chunkBuffer = Buffer.from(chunk);
709
+ else if (Buffer.isBuffer(chunk)) chunkBuffer = chunk;
710
+ else if (chunk instanceof Uint8Array) chunkBuffer = Buffer.from(chunk);
711
+ if (!chunkBuffer) return;
712
+ totalSize += chunkBuffer.length;
713
+ if (chunks && totalSize <= maxResponseBodySize) chunks.push(chunkBuffer);
714
+ else chunks = null;
674
715
  });
716
+ let finalized = false;
717
+ const finalizeCapture = () => {
718
+ if (finalized) return;
719
+ finalized = true;
720
+ captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, headers, url$1, maxResponseBodySize);
721
+ };
722
+ response.prependOnceListener("end", finalizeCapture);
723
+ response.prependOnceListener("close", finalizeCapture);
724
+ response.prependOnceListener("aborted", finalizeCapture);
675
725
  }
676
726
  if (originalResponseHook) originalResponseHook(span, response);
677
727
  };
@@ -925,11 +975,11 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
925
975
  const record = this._recordFromReq.get(request);
926
976
  if (!record) return;
927
977
  const { span } = record;
928
- const { remoteAddress, remotePort } = socket;
929
- const spanAttributes = {
930
- [_opentelemetry_semantic_conventions.ATTR_NETWORK_PEER_ADDRESS]: remoteAddress,
931
- [_opentelemetry_semantic_conventions.ATTR_NETWORK_PEER_PORT]: remotePort
932
- };
978
+ const spanAttributes = {};
979
+ const remoteAddress = typeof socket.remoteAddress === "string" ? socket.remoteAddress : void 0;
980
+ const remotePort = typeof socket.remotePort === "number" ? socket.remotePort : void 0;
981
+ if (remoteAddress) spanAttributes[_opentelemetry_semantic_conventions.ATTR_NETWORK_PEER_ADDRESS] = remoteAddress;
982
+ if (remotePort !== void 0) spanAttributes[_opentelemetry_semantic_conventions.ATTR_NETWORK_PEER_PORT] = remotePort;
933
983
  const headersMap = this.parseRequestHeaders(request);
934
984
  for (const [name, value] of headersMap.entries()) {
935
985
  const attrValue = Array.isArray(value) ? value.join(", ") : value;
@@ -998,14 +1048,15 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
998
1048
  this._diag.error("Error occurred while capturing request body:", e);
999
1049
  }
1000
1050
  }
1051
+ const errorMessage = error.message;
1001
1052
  span.recordException(error);
1002
1053
  span.setStatus({
1003
1054
  code: _opentelemetry_api.SpanStatusCode.ERROR,
1004
- message: error.message
1055
+ message: errorMessage
1005
1056
  });
1006
1057
  span.end();
1007
1058
  this._recordFromReq.delete(request);
1008
- attributes[_opentelemetry_semantic_conventions.ATTR_ERROR_TYPE] = error.message;
1059
+ attributes[_opentelemetry_semantic_conventions.ATTR_ERROR_TYPE] = errorMessage;
1009
1060
  this.recordRequestDuration(attributes, startTime);
1010
1061
  }
1011
1062
  onBodyChunkSent({ request, chunk }) {