@pingops/otel 0.2.4 → 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.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,7 +581,7 @@ 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;
@@ -608,17 +620,19 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
608
620
  if (headers) captureRequestHeaders(span, headers);
609
621
  if (request instanceof ClientRequest) {
610
622
  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;
623
+ const hostHeader = request.getHeader("host");
624
+ const host = typeof hostHeader === "string" ? hostHeader : Array.isArray(hostHeader) ? hostHeader.join(",") : typeof hostHeader === "number" ? String(hostHeader) : void 0;
625
+ const url = request.path && host ? `${request.protocol || "http:"}//${host}${request.path}` : void 0;
612
626
  const originalWrite = request.write.bind(request);
613
627
  const originalEnd = request.end.bind(request);
614
- request.write = (data) => {
628
+ request.write = ((data, ...rest) => {
615
629
  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) => {
630
+ return originalWrite(data, ...rest);
631
+ });
632
+ request.end = ((data, ...rest) => {
619
633
  if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url);
620
- return originalEnd(data);
621
- };
634
+ return originalEnd(data, ...rest);
635
+ });
622
636
  }
623
637
  if (originalRequestHook) originalRequestHook(span, request);
624
638
  };
@@ -635,15 +649,24 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
635
649
  const shouldCapture = shouldCaptureResponseBody$1(url);
636
650
  response.prependListener("data", (chunk) => {
637
651
  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);
652
+ let chunkBuffer = null;
653
+ if (typeof chunk === "string") chunkBuffer = Buffer.from(chunk);
654
+ else if (Buffer.isBuffer(chunk)) chunkBuffer = chunk;
655
+ else if (chunk instanceof Uint8Array) chunkBuffer = Buffer.from(chunk);
656
+ if (!chunkBuffer) return;
657
+ totalSize += chunkBuffer.length;
658
+ if (chunks && totalSize <= maxResponseBodySize) chunks.push(chunkBuffer);
659
+ else chunks = null;
646
660
  });
661
+ let finalized = false;
662
+ const finalizeCapture = () => {
663
+ if (finalized) return;
664
+ finalized = true;
665
+ captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, headers, url, maxResponseBodySize);
666
+ };
667
+ response.prependOnceListener("end", finalizeCapture);
668
+ response.prependOnceListener("close", finalizeCapture);
669
+ response.prependOnceListener("aborted", finalizeCapture);
647
670
  }
648
671
  if (originalResponseHook) originalResponseHook(span, response);
649
672
  };
@@ -897,11 +920,11 @@ var UndiciInstrumentation = class extends InstrumentationBase {
897
920
  const record = this._recordFromReq.get(request);
898
921
  if (!record) return;
899
922
  const { span } = record;
900
- const { remoteAddress, remotePort } = socket;
901
- const spanAttributes = {
902
- [ATTR_NETWORK_PEER_ADDRESS]: remoteAddress,
903
- [ATTR_NETWORK_PEER_PORT]: remotePort
904
- };
923
+ const spanAttributes = {};
924
+ const remoteAddress = typeof socket.remoteAddress === "string" ? socket.remoteAddress : void 0;
925
+ const remotePort = typeof socket.remotePort === "number" ? socket.remotePort : void 0;
926
+ if (remoteAddress) spanAttributes[ATTR_NETWORK_PEER_ADDRESS] = remoteAddress;
927
+ if (remotePort !== void 0) spanAttributes[ATTR_NETWORK_PEER_PORT] = remotePort;
905
928
  const headersMap = this.parseRequestHeaders(request);
906
929
  for (const [name, value] of headersMap.entries()) {
907
930
  const attrValue = Array.isArray(value) ? value.join(", ") : value;
@@ -970,14 +993,15 @@ var UndiciInstrumentation = class extends InstrumentationBase {
970
993
  this._diag.error("Error occurred while capturing request body:", e);
971
994
  }
972
995
  }
996
+ const errorMessage = error.message;
973
997
  span.recordException(error);
974
998
  span.setStatus({
975
999
  code: SpanStatusCode.ERROR,
976
- message: error.message
1000
+ message: errorMessage
977
1001
  });
978
1002
  span.end();
979
1003
  this._recordFromReq.delete(request);
980
- attributes[ATTR_ERROR_TYPE] = error.message;
1004
+ attributes[ATTR_ERROR_TYPE] = errorMessage;
981
1005
  this.recordRequestDuration(attributes, startTime);
982
1006
  }
983
1007
  onBodyChunkSent({ request, chunk }) {