@pingops/otel 0.2.3 → 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
@@ -645,6 +683,14 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
645
683
  /**
646
684
  * HTTP instrumentation for OpenTelemetry
647
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
+ }
648
694
  /**
649
695
  * Creates an HTTP instrumentation instance.
650
696
  * All outgoing HTTP requests are instrumented when the SDK is initialized.
@@ -654,12 +700,16 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
654
700
  */
655
701
  function createHttpInstrumentation(config) {
656
702
  const globalConfig$1 = getGlobalConfig();
703
+ const userIgnoreOutgoingRequestHook = config?.ignoreOutgoingRequestHook;
657
704
  return new PingopsHttpInstrumentation({
705
+ ...config,
658
706
  ignoreIncomingRequestHook: () => true,
659
- ignoreOutgoingRequestHook: () => false,
707
+ ignoreOutgoingRequestHook: (request) => {
708
+ if (shouldIgnoreOutboundInstrumentation(toRequestUrl$1(request))) return true;
709
+ return userIgnoreOutgoingRequestHook?.(request) ?? false;
710
+ },
660
711
  maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
661
- maxResponseBodySize: globalConfig$1?.maxResponseBodySize,
662
- ...config
712
+ maxResponseBodySize: globalConfig$1?.maxResponseBodySize
663
713
  });
664
714
  }
665
715
 
@@ -1038,6 +1088,13 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
1038
1088
  /**
1039
1089
  * Undici instrumentation for OpenTelemetry
1040
1090
  */
1091
+ function toRequestUrl(request) {
1092
+ try {
1093
+ return new URL(request.path, request.origin).toString();
1094
+ } catch {
1095
+ return;
1096
+ }
1097
+ }
1041
1098
  /**
1042
1099
  * Creates an Undici instrumentation instance.
1043
1100
  * All requests are instrumented when the SDK is initialized.
@@ -1048,7 +1105,9 @@ function createUndiciInstrumentation() {
1048
1105
  const globalConfig$1 = getGlobalConfig();
1049
1106
  return new UndiciInstrumentation({
1050
1107
  enabled: true,
1051
- ignoreRequestHook: () => false,
1108
+ ignoreRequestHook: (request) => {
1109
+ return shouldIgnoreOutboundInstrumentation(toRequestUrl(request));
1110
+ },
1052
1111
  maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
1053
1112
  maxResponseBodySize: globalConfig$1?.maxResponseBodySize
1054
1113
  });