@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 +82 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +83 -32
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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,
|
|
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 (
|
|
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 (
|
|
438
|
-
|
|
439
|
-
|
|
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
|
|
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[
|
|
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
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
|
|
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:
|
|
1027
|
+
message: errorMessage
|
|
977
1028
|
});
|
|
978
1029
|
span.end();
|
|
979
1030
|
this._recordFromReq.delete(request);
|
|
980
|
-
attributes[ATTR_ERROR_TYPE] =
|
|
1031
|
+
attributes[ATTR_ERROR_TYPE] = errorMessage;
|
|
981
1032
|
this.recordRequestDuration(attributes, startTime);
|
|
982
1033
|
}
|
|
983
1034
|
onBodyChunkSent({ request, chunk }) {
|