@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.mjs CHANGED
@@ -4,7 +4,7 @@ import { HTTP_RESPONSE_CONTENT_ENCODING, PINGOPS_CAPTURE_REQUEST_BODY, PINGOPS_C
4
4
  import { INVALID_SPAN_CONTEXT, ROOT_CONTEXT, SpanKind, SpanStatusCode, ValueType, context, propagation, trace } from "@opentelemetry/api";
5
5
  import "@opentelemetry/sdk-trace-node";
6
6
  import "@opentelemetry/resources";
7
- import { ATTR_ERROR_TYPE, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_REQUEST_METHOD_ORIGINAL, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_HTTP_URL, ATTR_NETWORK_PEER_ADDRESS, ATTR_NETWORK_PEER_PORT, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL, ATTR_URL_PATH, ATTR_URL_QUERY, ATTR_URL_SCHEME, ATTR_USER_AGENT_ORIGINAL, METRIC_HTTP_CLIENT_REQUEST_DURATION } from "@opentelemetry/semantic-conventions";
7
+ import { ATTR_ERROR_TYPE, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_REQUEST_METHOD_ORIGINAL, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_NETWORK_PEER_ADDRESS, ATTR_NETWORK_PEER_PORT, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL, ATTR_URL_PATH, ATTR_URL_QUERY, ATTR_URL_SCHEME, ATTR_USER_AGENT_ORIGINAL, METRIC_HTTP_CLIENT_REQUEST_DURATION } from "@opentelemetry/semantic-conventions";
8
8
  import { ClientRequest, IncomingMessage } from "http";
9
9
  import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
10
10
  import { hrTime, hrTimeDuration, hrTimeToMilliseconds, isTracingSuppressed } from "@opentelemetry/core";
@@ -411,6 +411,7 @@ function resolveOutboundSpanParentContext(activeContext, requestUrl) {
411
411
  */
412
412
  const DEFAULT_MAX_REQUEST_BODY_SIZE$1 = 4 * 1024;
413
413
  const DEFAULT_MAX_RESPONSE_BODY_SIZE$1 = 4 * 1024;
414
+ const LEGACY_ATTR_HTTP_URL = "http.url";
414
415
  const PingopsSemanticAttributes = {
415
416
  HTTP_REQUEST_BODY: "http.request.body",
416
417
  HTTP_RESPONSE_BODY: "http.response.body"
@@ -418,12 +419,18 @@ const PingopsSemanticAttributes = {
418
419
  /**
419
420
  * Manually flattens a nested object into dot-notation keys
420
421
  */
422
+ function isPlainObject(value) {
423
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Buffer);
424
+ }
425
+ function isPrimitiveArray(value) {
426
+ return value.every((item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean");
427
+ }
421
428
  function flatten(obj, prefix = "") {
422
429
  const result = {};
423
430
  for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) {
424
431
  const newKey = prefix ? `${prefix}.${key}` : key;
425
432
  const value = obj[key];
426
- if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Buffer)) Object.assign(result, flatten(value, newKey));
433
+ if (isPlainObject(value)) Object.assign(result, flatten(value, newKey));
427
434
  else result[newKey] = value;
428
435
  }
429
436
  return result;
@@ -434,11 +441,9 @@ function flatten(obj, prefix = "") {
434
441
  function setAttributeValue(span, attrName, attrValue) {
435
442
  if (typeof attrValue === "string" || typeof attrValue === "number" || typeof attrValue === "boolean") span.setAttribute(attrName, attrValue);
436
443
  else if (attrValue instanceof Buffer) span.setAttribute(attrName, attrValue.toString("utf8"));
437
- else if (typeof attrValue == "object") span.setAttributes(flatten({ [attrName]: attrValue }));
438
- else if (Array.isArray(attrValue)) if (attrValue.length) {
439
- const firstElement = attrValue[0];
440
- if (typeof firstElement === "string" || typeof firstElement === "number" || typeof firstElement === "boolean") span.setAttribute(attrName, attrValue);
441
- } else span.setAttribute(attrName, attrValue);
444
+ else if (Array.isArray(attrValue)) {
445
+ if (attrValue.length === 0 || isPrimitiveArray(attrValue)) span.setAttribute(attrName, attrValue);
446
+ } else if (isPlainObject(attrValue)) span.setAttributes(flatten({ [attrName]: attrValue }));
442
447
  }
443
448
  /**
444
449
  * Extracts domain from URL
@@ -504,9 +509,16 @@ function captureRequestBody(span, data, maxSize, semanticAttr, url) {
504
509
  /**
505
510
  * Captures response body from chunks
506
511
  */
507
- function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url) {
512
+ function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url, maxSize) {
508
513
  if (!shouldCaptureResponseBody$1(url)) return;
509
- if (chunks && chunks.length) try {
514
+ if (chunks === null) {
515
+ const contentEncoding = responseHeaders?.["content-encoding"];
516
+ const contentType = responseHeaders?.["content-type"];
517
+ const toHeaderString = (value) => typeof value === "string" ? value : Array.isArray(value) ? value.join(", ") : "unknown";
518
+ setAttributeValue(span, semanticAttr, `[truncated response body; exceeded maxResponseBodySize=${maxSize ?? DEFAULT_MAX_RESPONSE_BODY_SIZE$1}; content-type=${toHeaderString(contentType)}; content-encoding=${toHeaderString(contentEncoding)}]`);
519
+ return;
520
+ }
521
+ if (chunks.length) try {
510
522
  const concatedChunks = Buffer.concat(chunks);
511
523
  const contentEncoding = responseHeaders?.["content-encoding"];
512
524
  if (isCompressedContentEncoding(contentEncoding)) {
@@ -569,12 +581,27 @@ function extractRequestUrlFromSpanOptions(options) {
569
581
  const attrs = options.attributes;
570
582
  if (!attrs) return;
571
583
  if (typeof attrs[ATTR_URL_FULL] === "string") return attrs[ATTR_URL_FULL];
572
- if (typeof attrs[ATTR_HTTP_URL] === "string") return attrs[ATTR_HTTP_URL];
584
+ if (typeof attrs[LEGACY_ATTR_HTTP_URL] === "string") return attrs[LEGACY_ATTR_HTTP_URL];
573
585
  const scheme = typeof attrs[ATTR_URL_SCHEME] === "string" ? attrs[ATTR_URL_SCHEME] : "http";
574
586
  const host = typeof attrs[ATTR_SERVER_ADDRESS] === "string" ? attrs[ATTR_SERVER_ADDRESS] : void 0;
575
587
  if (!host) return;
576
588
  return `${scheme}://${host}${typeof attrs[ATTR_SERVER_PORT] === "number" ? `:${attrs[ATTR_SERVER_PORT]}` : ""}${typeof attrs[ATTR_URL_PATH] === "string" ? attrs[ATTR_URL_PATH] : "/"}${typeof attrs[ATTR_URL_QUERY] === "string" && attrs[ATTR_URL_QUERY].length > 0 ? `?${attrs[ATTR_URL_QUERY]}` : ""}`;
577
589
  }
590
+ function parseRequestPathAndQuery(pathWithQuery) {
591
+ const queryIndex = pathWithQuery.indexOf("?");
592
+ if (queryIndex < 0) return { path: pathWithQuery || "/" };
593
+ const path = pathWithQuery.slice(0, queryIndex) || "/";
594
+ const queryPart = pathWithQuery.slice(queryIndex);
595
+ return {
596
+ path,
597
+ query: queryPart.length > 0 ? queryPart : void 0
598
+ };
599
+ }
600
+ function extractClientRequestPath(request) {
601
+ if (!request || typeof request !== "object" || !("path" in request)) return;
602
+ const path = request.path;
603
+ return typeof path === "string" && path.length > 0 ? path : void 0;
604
+ }
578
605
  const PingopsHttpSemanticAttributes = PingopsSemanticAttributes;
579
606
  var PingopsHttpInstrumentation = class extends HttpInstrumentation {
580
607
  constructor(config) {
@@ -608,17 +635,25 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
608
635
  if (headers) captureRequestHeaders(span, headers);
609
636
  if (request instanceof ClientRequest) {
610
637
  const maxRequestBodySize = config?.maxRequestBodySize || DEFAULT_MAX_REQUEST_BODY_SIZE$1;
611
- const url = request.path && request.getHeader("host") ? `${request.protocol || "http:"}//${request.getHeader("host")}${request.path}` : void 0;
638
+ const hostHeader = request.getHeader("host");
639
+ const host = typeof hostHeader === "string" ? hostHeader : Array.isArray(hostHeader) ? hostHeader.join(",") : typeof hostHeader === "number" ? String(hostHeader) : void 0;
640
+ const url = request.path && host ? `${request.protocol || "http:"}//${host}${request.path}` : void 0;
641
+ if (typeof request.path === "string" && request.path.length > 0) {
642
+ const { path, query } = parseRequestPathAndQuery(request.path);
643
+ span.setAttribute(ATTR_URL_PATH, path);
644
+ if (query) span.setAttribute(ATTR_URL_QUERY, query);
645
+ }
646
+ if (url) span.setAttribute(ATTR_URL_FULL, url);
612
647
  const originalWrite = request.write.bind(request);
613
648
  const originalEnd = request.end.bind(request);
614
- request.write = (data) => {
649
+ request.write = ((data, ...rest) => {
615
650
  if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url);
616
- return originalWrite(data);
617
- };
618
- request.end = (data) => {
651
+ return originalWrite(data, ...rest);
652
+ });
653
+ request.end = ((data, ...rest) => {
619
654
  if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url);
620
- return originalEnd(data);
621
- };
655
+ return originalEnd(data, ...rest);
656
+ });
622
657
  }
623
658
  if (originalRequestHook) originalRequestHook(span, request);
624
659
  };
@@ -628,6 +663,12 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
628
663
  const headers = extractResponseHeaders(response);
629
664
  if (headers) captureResponseHeaders(span, headers);
630
665
  if (response instanceof IncomingMessage) {
666
+ const requestPath = response.req instanceof ClientRequest ? extractClientRequestPath(response.req) : void 0;
667
+ if (requestPath) {
668
+ const { path, query } = parseRequestPathAndQuery(requestPath);
669
+ span.setAttribute(ATTR_URL_PATH, path);
670
+ if (query) span.setAttribute(ATTR_URL_QUERY, query);
671
+ }
631
672
  const maxResponseBodySize = config?.maxResponseBodySize || DEFAULT_MAX_RESPONSE_BODY_SIZE$1;
632
673
  const url = response.url || void 0;
633
674
  let chunks = [];
@@ -635,15 +676,24 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
635
676
  const shouldCapture = shouldCaptureResponseBody$1(url);
636
677
  response.prependListener("data", (chunk) => {
637
678
  if (!chunk || !shouldCapture) return;
638
- if (typeof chunk === "string" || chunk instanceof Buffer) {
639
- totalSize += chunk.length;
640
- if (chunks && totalSize <= maxResponseBodySize) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
641
- else chunks = null;
642
- }
643
- });
644
- response.prependOnceListener("end", () => {
645
- captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, headers, url);
679
+ let chunkBuffer = null;
680
+ if (typeof chunk === "string") chunkBuffer = Buffer.from(chunk);
681
+ else if (Buffer.isBuffer(chunk)) chunkBuffer = chunk;
682
+ else if (chunk instanceof Uint8Array) chunkBuffer = Buffer.from(chunk);
683
+ if (!chunkBuffer) return;
684
+ totalSize += chunkBuffer.length;
685
+ if (chunks && totalSize <= maxResponseBodySize) chunks.push(chunkBuffer);
686
+ else chunks = null;
646
687
  });
688
+ let finalized = false;
689
+ const finalizeCapture = () => {
690
+ if (finalized) return;
691
+ finalized = true;
692
+ captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, headers, url, maxResponseBodySize);
693
+ };
694
+ response.prependOnceListener("end", finalizeCapture);
695
+ response.prependOnceListener("close", finalizeCapture);
696
+ response.prependOnceListener("aborted", finalizeCapture);
647
697
  }
648
698
  if (originalResponseHook) originalResponseHook(span, response);
649
699
  };
@@ -897,11 +947,11 @@ var UndiciInstrumentation = class extends InstrumentationBase {
897
947
  const record = this._recordFromReq.get(request);
898
948
  if (!record) return;
899
949
  const { span } = record;
900
- const { remoteAddress, remotePort } = socket;
901
- const spanAttributes = {
902
- [ATTR_NETWORK_PEER_ADDRESS]: remoteAddress,
903
- [ATTR_NETWORK_PEER_PORT]: remotePort
904
- };
950
+ const spanAttributes = {};
951
+ const remoteAddress = typeof socket.remoteAddress === "string" ? socket.remoteAddress : void 0;
952
+ const remotePort = typeof socket.remotePort === "number" ? socket.remotePort : void 0;
953
+ if (remoteAddress) spanAttributes[ATTR_NETWORK_PEER_ADDRESS] = remoteAddress;
954
+ if (remotePort !== void 0) spanAttributes[ATTR_NETWORK_PEER_PORT] = remotePort;
905
955
  const headersMap = this.parseRequestHeaders(request);
906
956
  for (const [name, value] of headersMap.entries()) {
907
957
  const attrValue = Array.isArray(value) ? value.join(", ") : value;
@@ -970,14 +1020,15 @@ var UndiciInstrumentation = class extends InstrumentationBase {
970
1020
  this._diag.error("Error occurred while capturing request body:", e);
971
1021
  }
972
1022
  }
1023
+ const errorMessage = error.message;
973
1024
  span.recordException(error);
974
1025
  span.setStatus({
975
1026
  code: SpanStatusCode.ERROR,
976
- message: error.message
1027
+ message: errorMessage
977
1028
  });
978
1029
  span.end();
979
1030
  this._recordFromReq.delete(request);
980
- attributes[ATTR_ERROR_TYPE] = error.message;
1031
+ attributes[ATTR_ERROR_TYPE] = errorMessage;
981
1032
  this.recordRequestDuration(attributes, startTime);
982
1033
  }
983
1034
  onBodyChunkSent({ request, chunk }) {