@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.cjs CHANGED
@@ -60,6 +60,21 @@ function getGlobalConfig() {
60
60
  //#endregion
61
61
  //#region src/span-processor.ts
62
62
  const logger$2 = (0, _pingops_core.createLogger)("[PingOps Processor]");
63
+ function normalizePath$1(pathname) {
64
+ return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
65
+ }
66
+ function isExporterRequestUrl$1(url$1, exporterUrl) {
67
+ try {
68
+ const request = new URL(url$1);
69
+ const exporter = new URL(exporterUrl);
70
+ if (request.origin !== exporter.origin) return false;
71
+ const requestPath = normalizePath$1(request.pathname);
72
+ const exporterPath = normalizePath$1(exporter.pathname);
73
+ return requestPath === exporterPath || requestPath.startsWith(`${exporterPath}/`);
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
63
78
  /**
64
79
  * Creates a filtered span wrapper that applies header filtering to attributes
65
80
  *
@@ -93,6 +108,7 @@ function createFilteredSpan(span, domainAllowList, globalHeadersAllowList, globa
93
108
  */
94
109
  var PingopsSpanProcessor = class {
95
110
  processor;
111
+ exporterTraceUrl;
96
112
  config;
97
113
  /**
98
114
  * Creates a new PingopsSpanProcessor instance.
@@ -102,8 +118,9 @@ var PingopsSpanProcessor = class {
102
118
  constructor(config) {
103
119
  const exportMode = config.exportMode ?? "batched";
104
120
  const apiKey = config.apiKey || process.env.PINGOPS_API_KEY || "";
121
+ this.exporterTraceUrl = `${config.baseUrl}/v1/traces`;
105
122
  const exporter = new _opentelemetry_exporter_trace_otlp_http.OTLPTraceExporter({
106
- url: `${config.baseUrl}/v1/traces`,
123
+ url: this.exporterTraceUrl,
107
124
  headers: {
108
125
  Authorization: apiKey ? `Bearer ${apiKey}` : "",
109
126
  "Content-Type": "application/json"
@@ -131,7 +148,7 @@ var PingopsSpanProcessor = class {
131
148
  domainAllowList: config.domainAllowList,
132
149
  maxRequestBodySize: config.maxRequestBodySize,
133
150
  maxResponseBodySize: config.maxResponseBodySize,
134
- exportTraceUrl: `${config.baseUrl}/v1/traces`
151
+ exportTraceUrl: this.exporterTraceUrl
135
152
  });
136
153
  logger$2.info("Initialized PingopsSpanProcessor", {
137
154
  baseUrl: config.baseUrl,
@@ -192,6 +209,14 @@ var PingopsSpanProcessor = class {
192
209
  }
193
210
  const attributes = span.attributes;
194
211
  const url$1 = (0, _pingops_core.getHttpUrlFromAttributes)(attributes) ?? "";
212
+ if (url$1 && isExporterRequestUrl$1(url$1, this.exporterTraceUrl)) {
213
+ logger$2.debug("Skipping exporter span to prevent self-instrumentation", {
214
+ spanName: span.name,
215
+ spanId: spanContext.spanId,
216
+ url: url$1
217
+ });
218
+ return;
219
+ }
195
220
  logger$2.debug("Extracted URL for domain filtering", {
196
221
  spanName: span.name,
197
222
  url: url$1,
@@ -363,9 +388,12 @@ async function shutdownTracerProvider() {
363
388
  //#region src/instrumentations/suppression-guard.ts
364
389
  const logger = (0, _pingops_core.createLogger)("[PingOps SuppressionGuard]");
365
390
  let hasLoggedSuppressionLeakWarning = false;
366
- function normalizeUrl(url$1) {
391
+ function normalizePath(pathname) {
392
+ return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
393
+ }
394
+ function parseUrl(url$1) {
367
395
  try {
368
- return new URL(url$1).toString();
396
+ return new URL(url$1);
369
397
  } catch {
370
398
  return null;
371
399
  }
@@ -374,10 +402,20 @@ function isExporterRequestUrl(requestUrl) {
374
402
  if (!requestUrl) return false;
375
403
  const exporterUrl = getGlobalConfig()?.exportTraceUrl;
376
404
  if (!exporterUrl) return false;
377
- const normalizedRequestUrl = normalizeUrl(requestUrl);
378
- const normalizedExporterUrl = normalizeUrl(exporterUrl);
379
- if (!normalizedRequestUrl || !normalizedExporterUrl) return false;
380
- return normalizedRequestUrl.startsWith(normalizedExporterUrl);
405
+ const parsedRequestUrl = parseUrl(requestUrl);
406
+ const parsedExporterUrl = parseUrl(exporterUrl);
407
+ if (!parsedRequestUrl || !parsedExporterUrl) return false;
408
+ if (parsedRequestUrl.origin !== parsedExporterUrl.origin) return false;
409
+ const requestPath = normalizePath(parsedRequestUrl.pathname);
410
+ const exporterPath = normalizePath(parsedExporterUrl.pathname);
411
+ return requestPath === exporterPath || requestPath.startsWith(`${exporterPath}/`);
412
+ }
413
+ /**
414
+ * Determines whether an outbound request should be skipped from instrumentation
415
+ * to prevent exporter self-instrumentation loops.
416
+ */
417
+ function shouldIgnoreOutboundInstrumentation(requestUrl) {
418
+ return isExporterRequestUrl(requestUrl);
381
419
  }
382
420
  /**
383
421
  * Returns a context for outbound span creation that neutralizes leaked suppression
@@ -555,6 +593,16 @@ function captureRequestHeaders(span, headers) {
555
593
  function captureResponseHeaders(span, headers) {
556
594
  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));
557
595
  }
596
+ function extractRequestUrlFromSpanOptions(options) {
597
+ const attrs = options.attributes;
598
+ if (!attrs) return;
599
+ 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];
601
+ const scheme = typeof attrs[_opentelemetry_semantic_conventions.ATTR_URL_SCHEME] === "string" ? attrs[_opentelemetry_semantic_conventions.ATTR_URL_SCHEME] : "http";
602
+ const host = typeof attrs[_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS] === "string" ? attrs[_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS] : void 0;
603
+ if (!host) return;
604
+ 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
+ }
558
606
  const PingopsHttpSemanticAttributes = PingopsSemanticAttributes;
559
607
  var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_http.HttpInstrumentation {
560
608
  constructor(config) {
@@ -572,7 +620,7 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
572
620
  const originalStartHttpSpan = target._startHttpSpan.bind(this);
573
621
  target._startHttpSpan = (name, options, ctx = _opentelemetry_api.context.active()) => {
574
622
  if (options.kind !== _opentelemetry_api.SpanKind.CLIENT) return originalStartHttpSpan(name, options, ctx);
575
- return originalStartHttpSpan(name, options, resolveOutboundSpanParentContext(ctx, typeof options.attributes?.["url.full"] === "string" ? options.attributes["url.full"] : void 0));
623
+ return originalStartHttpSpan(name, options, resolveOutboundSpanParentContext(ctx, extractRequestUrlFromSpanOptions(options)));
576
624
  };
577
625
  }
578
626
  _createConfig(config) {
@@ -635,6 +683,14 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
635
683
  /**
636
684
  * HTTP instrumentation for OpenTelemetry
637
685
  */
686
+ function toRequestUrl$1(request) {
687
+ if (typeof request.href === "string" && request.href.length > 0) return request.href;
688
+ const protocol = typeof request.protocol === "string" && request.protocol.length > 0 ? request.protocol : "http:";
689
+ const hostnameOrHost = typeof request.hostname === "string" && request.hostname.length > 0 ? request.hostname : request.host;
690
+ if (!hostnameOrHost) return;
691
+ const hasPortInHost = hostnameOrHost.includes(":");
692
+ return `${protocol}//${hostnameOrHost}${request.port != null && !hasPortInHost ? `:${request.port}` : ""}${typeof request.path === "string" ? request.path : typeof request.pathname === "string" ? request.pathname : "/"}`;
693
+ }
638
694
  /**
639
695
  * Creates an HTTP instrumentation instance.
640
696
  * All outgoing HTTP requests are instrumented when the SDK is initialized.
@@ -644,12 +700,16 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
644
700
  */
645
701
  function createHttpInstrumentation(config) {
646
702
  const globalConfig$1 = getGlobalConfig();
703
+ const userIgnoreOutgoingRequestHook = config?.ignoreOutgoingRequestHook;
647
704
  return new PingopsHttpInstrumentation({
705
+ ...config,
648
706
  ignoreIncomingRequestHook: () => true,
649
- ignoreOutgoingRequestHook: () => false,
707
+ ignoreOutgoingRequestHook: (request) => {
708
+ if (shouldIgnoreOutboundInstrumentation(toRequestUrl$1(request))) return true;
709
+ return userIgnoreOutgoingRequestHook?.(request) ?? false;
710
+ },
650
711
  maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
651
- maxResponseBodySize: globalConfig$1?.maxResponseBodySize,
652
- ...config
712
+ maxResponseBodySize: globalConfig$1?.maxResponseBodySize
653
713
  });
654
714
  }
655
715
 
@@ -1028,6 +1088,13 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
1028
1088
  /**
1029
1089
  * Undici instrumentation for OpenTelemetry
1030
1090
  */
1091
+ function toRequestUrl(request) {
1092
+ try {
1093
+ return new URL(request.path, request.origin).toString();
1094
+ } catch {
1095
+ return;
1096
+ }
1097
+ }
1031
1098
  /**
1032
1099
  * Creates an Undici instrumentation instance.
1033
1100
  * All requests are instrumented when the SDK is initialized.
@@ -1038,7 +1105,9 @@ function createUndiciInstrumentation() {
1038
1105
  const globalConfig$1 = getGlobalConfig();
1039
1106
  return new UndiciInstrumentation({
1040
1107
  enabled: true,
1041
- ignoreRequestHook: () => false,
1108
+ ignoreRequestHook: (request) => {
1109
+ return shouldIgnoreOutboundInstrumentation(toRequestUrl(request));
1110
+ },
1042
1111
  maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
1043
1112
  maxResponseBodySize: globalConfig$1?.maxResponseBodySize
1044
1113
  });