@pingops/otel 0.2.2 → 0.2.4

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_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_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";
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";
@@ -32,6 +32,21 @@ function getGlobalConfig() {
32
32
  //#endregion
33
33
  //#region src/span-processor.ts
34
34
  const logger$2 = createLogger("[PingOps Processor]");
35
+ function normalizePath$1(pathname) {
36
+ return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
37
+ }
38
+ function isExporterRequestUrl$1(url, exporterUrl) {
39
+ try {
40
+ const request = new URL(url);
41
+ const exporter = new URL(exporterUrl);
42
+ if (request.origin !== exporter.origin) return false;
43
+ const requestPath = normalizePath$1(request.pathname);
44
+ const exporterPath = normalizePath$1(exporter.pathname);
45
+ return requestPath === exporterPath || requestPath.startsWith(`${exporterPath}/`);
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
35
50
  /**
36
51
  * Creates a filtered span wrapper that applies header filtering to attributes
37
52
  *
@@ -65,6 +80,7 @@ function createFilteredSpan(span, domainAllowList, globalHeadersAllowList, globa
65
80
  */
66
81
  var PingopsSpanProcessor = class {
67
82
  processor;
83
+ exporterTraceUrl;
68
84
  config;
69
85
  /**
70
86
  * Creates a new PingopsSpanProcessor instance.
@@ -74,8 +90,9 @@ var PingopsSpanProcessor = class {
74
90
  constructor(config) {
75
91
  const exportMode = config.exportMode ?? "batched";
76
92
  const apiKey = config.apiKey || process.env.PINGOPS_API_KEY || "";
93
+ this.exporterTraceUrl = `${config.baseUrl}/v1/traces`;
77
94
  const exporter = new OTLPTraceExporter({
78
- url: `${config.baseUrl}/v1/traces`,
95
+ url: this.exporterTraceUrl,
79
96
  headers: {
80
97
  Authorization: apiKey ? `Bearer ${apiKey}` : "",
81
98
  "Content-Type": "application/json"
@@ -103,7 +120,7 @@ var PingopsSpanProcessor = class {
103
120
  domainAllowList: config.domainAllowList,
104
121
  maxRequestBodySize: config.maxRequestBodySize,
105
122
  maxResponseBodySize: config.maxResponseBodySize,
106
- exportTraceUrl: `${config.baseUrl}/v1/traces`
123
+ exportTraceUrl: this.exporterTraceUrl
107
124
  });
108
125
  logger$2.info("Initialized PingopsSpanProcessor", {
109
126
  baseUrl: config.baseUrl,
@@ -164,6 +181,14 @@ var PingopsSpanProcessor = class {
164
181
  }
165
182
  const attributes = span.attributes;
166
183
  const url = getHttpUrlFromAttributes(attributes) ?? "";
184
+ if (url && isExporterRequestUrl$1(url, this.exporterTraceUrl)) {
185
+ logger$2.debug("Skipping exporter span to prevent self-instrumentation", {
186
+ spanName: span.name,
187
+ spanId: spanContext.spanId,
188
+ url
189
+ });
190
+ return;
191
+ }
167
192
  logger$2.debug("Extracted URL for domain filtering", {
168
193
  spanName: span.name,
169
194
  url,
@@ -335,9 +360,12 @@ async function shutdownTracerProvider() {
335
360
  //#region src/instrumentations/suppression-guard.ts
336
361
  const logger = createLogger("[PingOps SuppressionGuard]");
337
362
  let hasLoggedSuppressionLeakWarning = false;
338
- function normalizeUrl(url) {
363
+ function normalizePath(pathname) {
364
+ return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
365
+ }
366
+ function parseUrl(url) {
339
367
  try {
340
- return new URL(url).toString();
368
+ return new URL(url);
341
369
  } catch {
342
370
  return null;
343
371
  }
@@ -346,10 +374,20 @@ function isExporterRequestUrl(requestUrl) {
346
374
  if (!requestUrl) return false;
347
375
  const exporterUrl = getGlobalConfig()?.exportTraceUrl;
348
376
  if (!exporterUrl) return false;
349
- const normalizedRequestUrl = normalizeUrl(requestUrl);
350
- const normalizedExporterUrl = normalizeUrl(exporterUrl);
351
- if (!normalizedRequestUrl || !normalizedExporterUrl) return false;
352
- return normalizedRequestUrl.startsWith(normalizedExporterUrl);
377
+ const parsedRequestUrl = parseUrl(requestUrl);
378
+ const parsedExporterUrl = parseUrl(exporterUrl);
379
+ if (!parsedRequestUrl || !parsedExporterUrl) return false;
380
+ if (parsedRequestUrl.origin !== parsedExporterUrl.origin) return false;
381
+ const requestPath = normalizePath(parsedRequestUrl.pathname);
382
+ const exporterPath = normalizePath(parsedExporterUrl.pathname);
383
+ return requestPath === exporterPath || requestPath.startsWith(`${exporterPath}/`);
384
+ }
385
+ /**
386
+ * Determines whether an outbound request should be skipped from instrumentation
387
+ * to prevent exporter self-instrumentation loops.
388
+ */
389
+ function shouldIgnoreOutboundInstrumentation(requestUrl) {
390
+ return isExporterRequestUrl(requestUrl);
353
391
  }
354
392
  /**
355
393
  * Returns a context for outbound span creation that neutralizes leaked suppression
@@ -527,6 +565,16 @@ function captureRequestHeaders(span, headers) {
527
565
  function captureResponseHeaders(span, headers) {
528
566
  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));
529
567
  }
568
+ function extractRequestUrlFromSpanOptions(options) {
569
+ const attrs = options.attributes;
570
+ if (!attrs) return;
571
+ 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];
573
+ const scheme = typeof attrs[ATTR_URL_SCHEME] === "string" ? attrs[ATTR_URL_SCHEME] : "http";
574
+ const host = typeof attrs[ATTR_SERVER_ADDRESS] === "string" ? attrs[ATTR_SERVER_ADDRESS] : void 0;
575
+ if (!host) return;
576
+ 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
+ }
530
578
  const PingopsHttpSemanticAttributes = PingopsSemanticAttributes;
531
579
  var PingopsHttpInstrumentation = class extends HttpInstrumentation {
532
580
  constructor(config) {
@@ -544,7 +592,7 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
544
592
  const originalStartHttpSpan = target._startHttpSpan.bind(this);
545
593
  target._startHttpSpan = (name, options, ctx = context.active()) => {
546
594
  if (options.kind !== SpanKind.CLIENT) return originalStartHttpSpan(name, options, ctx);
547
- return originalStartHttpSpan(name, options, resolveOutboundSpanParentContext(ctx, typeof options.attributes?.["url.full"] === "string" ? options.attributes["url.full"] : void 0));
595
+ return originalStartHttpSpan(name, options, resolveOutboundSpanParentContext(ctx, extractRequestUrlFromSpanOptions(options)));
548
596
  };
549
597
  }
550
598
  _createConfig(config) {
@@ -607,6 +655,14 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
607
655
  /**
608
656
  * HTTP instrumentation for OpenTelemetry
609
657
  */
658
+ function toRequestUrl$1(request) {
659
+ if (typeof request.href === "string" && request.href.length > 0) return request.href;
660
+ const protocol = typeof request.protocol === "string" && request.protocol.length > 0 ? request.protocol : "http:";
661
+ const hostnameOrHost = typeof request.hostname === "string" && request.hostname.length > 0 ? request.hostname : request.host;
662
+ if (!hostnameOrHost) return;
663
+ const hasPortInHost = hostnameOrHost.includes(":");
664
+ return `${protocol}//${hostnameOrHost}${request.port != null && !hasPortInHost ? `:${request.port}` : ""}${typeof request.path === "string" ? request.path : typeof request.pathname === "string" ? request.pathname : "/"}`;
665
+ }
610
666
  /**
611
667
  * Creates an HTTP instrumentation instance.
612
668
  * All outgoing HTTP requests are instrumented when the SDK is initialized.
@@ -616,12 +672,16 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
616
672
  */
617
673
  function createHttpInstrumentation(config) {
618
674
  const globalConfig$1 = getGlobalConfig();
675
+ const userIgnoreOutgoingRequestHook = config?.ignoreOutgoingRequestHook;
619
676
  return new PingopsHttpInstrumentation({
677
+ ...config,
620
678
  ignoreIncomingRequestHook: () => true,
621
- ignoreOutgoingRequestHook: () => false,
679
+ ignoreOutgoingRequestHook: (request) => {
680
+ if (shouldIgnoreOutboundInstrumentation(toRequestUrl$1(request))) return true;
681
+ return userIgnoreOutgoingRequestHook?.(request) ?? false;
682
+ },
622
683
  maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
623
- maxResponseBodySize: globalConfig$1?.maxResponseBodySize,
624
- ...config
684
+ maxResponseBodySize: globalConfig$1?.maxResponseBodySize
625
685
  });
626
686
  }
627
687
 
@@ -1000,6 +1060,13 @@ var UndiciInstrumentation = class extends InstrumentationBase {
1000
1060
  /**
1001
1061
  * Undici instrumentation for OpenTelemetry
1002
1062
  */
1063
+ function toRequestUrl(request) {
1064
+ try {
1065
+ return new URL(request.path, request.origin).toString();
1066
+ } catch {
1067
+ return;
1068
+ }
1069
+ }
1003
1070
  /**
1004
1071
  * Creates an Undici instrumentation instance.
1005
1072
  * All requests are instrumented when the SDK is initialized.
@@ -1010,7 +1077,9 @@ function createUndiciInstrumentation() {
1010
1077
  const globalConfig$1 = getGlobalConfig();
1011
1078
  return new UndiciInstrumentation({
1012
1079
  enabled: true,
1013
- ignoreRequestHook: () => false,
1080
+ ignoreRequestHook: (request) => {
1081
+ return shouldIgnoreOutboundInstrumentation(toRequestUrl(request));
1082
+ },
1014
1083
  maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
1015
1084
  maxResponseBodySize: globalConfig$1?.maxResponseBodySize
1016
1085
  });