@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.mjs CHANGED
@@ -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
@@ -617,6 +655,14 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
617
655
  /**
618
656
  * HTTP instrumentation for OpenTelemetry
619
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
+ }
620
666
  /**
621
667
  * Creates an HTTP instrumentation instance.
622
668
  * All outgoing HTTP requests are instrumented when the SDK is initialized.
@@ -626,12 +672,16 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
626
672
  */
627
673
  function createHttpInstrumentation(config) {
628
674
  const globalConfig$1 = getGlobalConfig();
675
+ const userIgnoreOutgoingRequestHook = config?.ignoreOutgoingRequestHook;
629
676
  return new PingopsHttpInstrumentation({
677
+ ...config,
630
678
  ignoreIncomingRequestHook: () => true,
631
- ignoreOutgoingRequestHook: () => false,
679
+ ignoreOutgoingRequestHook: (request) => {
680
+ if (shouldIgnoreOutboundInstrumentation(toRequestUrl$1(request))) return true;
681
+ return userIgnoreOutgoingRequestHook?.(request) ?? false;
682
+ },
632
683
  maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
633
- maxResponseBodySize: globalConfig$1?.maxResponseBodySize,
634
- ...config
684
+ maxResponseBodySize: globalConfig$1?.maxResponseBodySize
635
685
  });
636
686
  }
637
687
 
@@ -1010,6 +1060,13 @@ var UndiciInstrumentation = class extends InstrumentationBase {
1010
1060
  /**
1011
1061
  * Undici instrumentation for OpenTelemetry
1012
1062
  */
1063
+ function toRequestUrl(request) {
1064
+ try {
1065
+ return new URL(request.path, request.origin).toString();
1066
+ } catch {
1067
+ return;
1068
+ }
1069
+ }
1013
1070
  /**
1014
1071
  * Creates an Undici instrumentation instance.
1015
1072
  * All requests are instrumented when the SDK is initialized.
@@ -1020,7 +1077,9 @@ function createUndiciInstrumentation() {
1020
1077
  const globalConfig$1 = getGlobalConfig();
1021
1078
  return new UndiciInstrumentation({
1022
1079
  enabled: true,
1023
- ignoreRequestHook: () => false,
1080
+ ignoreRequestHook: (request) => {
1081
+ return shouldIgnoreOutboundInstrumentation(toRequestUrl(request));
1082
+ },
1024
1083
  maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
1025
1084
  maxResponseBodySize: globalConfig$1?.maxResponseBodySize
1026
1085
  });