@pingops/otel 0.2.1 → 0.2.2

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
@@ -34,11 +34,11 @@ require("@opentelemetry/resources");
34
34
  let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
35
35
  let http = require("http");
36
36
  let _opentelemetry_instrumentation_http = require("@opentelemetry/instrumentation-http");
37
+ let _opentelemetry_core = require("@opentelemetry/core");
37
38
  let diagnostics_channel = require("diagnostics_channel");
38
39
  diagnostics_channel = __toESM(diagnostics_channel);
39
40
  let url = require("url");
40
41
  let _opentelemetry_instrumentation = require("@opentelemetry/instrumentation");
41
- let _opentelemetry_core = require("@opentelemetry/core");
42
42
 
43
43
  //#region src/config-store.ts
44
44
  let globalConfig = null;
@@ -59,7 +59,7 @@ function getGlobalConfig() {
59
59
 
60
60
  //#endregion
61
61
  //#region src/span-processor.ts
62
- const logger$1 = (0, _pingops_core.createLogger)("[PingOps Processor]");
62
+ const logger$2 = (0, _pingops_core.createLogger)("[PingOps Processor]");
63
63
  /**
64
64
  * Creates a filtered span wrapper that applies header filtering to attributes
65
65
  *
@@ -77,7 +77,7 @@ const logger$1 = (0, _pingops_core.createLogger)("[PingOps Processor]");
77
77
  function createFilteredSpan(span, domainAllowList, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction) {
78
78
  const payload = (0, _pingops_core.extractSpanPayload)(span, domainAllowList, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction);
79
79
  const filteredAttributes = payload?.attributes ?? span.attributes;
80
- logger$1.debug("Payload", { payload });
80
+ logger$2.debug("Payload", { payload });
81
81
  return new Proxy(span, { get(target, prop) {
82
82
  if (prop === "attributes") return filteredAttributes;
83
83
  const value = target[prop];
@@ -130,9 +130,10 @@ var PingopsSpanProcessor = class {
130
130
  captureResponseBody: config.captureResponseBody,
131
131
  domainAllowList: config.domainAllowList,
132
132
  maxRequestBodySize: config.maxRequestBodySize,
133
- maxResponseBodySize: config.maxResponseBodySize
133
+ maxResponseBodySize: config.maxResponseBodySize,
134
+ exportTraceUrl: `${config.baseUrl}/v1/traces`
134
135
  });
135
- logger$1.info("Initialized PingopsSpanProcessor", {
136
+ logger$2.info("Initialized PingopsSpanProcessor", {
136
137
  baseUrl: config.baseUrl,
137
138
  exportMode,
138
139
  batchSize: config.batchSize,
@@ -148,7 +149,7 @@ var PingopsSpanProcessor = class {
148
149
  */
149
150
  onStart(span, parentContext) {
150
151
  const spanContext = span.spanContext();
151
- logger$1.debug("Span started", {
152
+ logger$2.debug("Span started", {
152
153
  spanName: span.name,
153
154
  spanId: spanContext.spanId,
154
155
  traceId: spanContext.traceId
@@ -156,7 +157,7 @@ var PingopsSpanProcessor = class {
156
157
  const propagatedAttributes = (0, _pingops_core.getPropagatedAttributesFromContext)(parentContext);
157
158
  if (Object.keys(propagatedAttributes).length > 0) {
158
159
  for (const [key, value] of Object.entries(propagatedAttributes)) if (typeof value === "string" || Array.isArray(value)) span.setAttribute(key, value);
159
- logger$1.debug("Set propagated attributes on span", {
160
+ logger$2.debug("Set propagated attributes on span", {
160
161
  spanName: span.name,
161
162
  attributeKeys: Object.keys(propagatedAttributes)
162
163
  });
@@ -174,7 +175,7 @@ var PingopsSpanProcessor = class {
174
175
  */
175
176
  onEnd(span) {
176
177
  const spanContext = span.spanContext();
177
- logger$1.debug("Span ended, processing", {
178
+ logger$2.debug("Span ended, processing", {
178
179
  spanName: span.name,
179
180
  spanId: spanContext.spanId,
180
181
  traceId: spanContext.traceId,
@@ -182,7 +183,7 @@ var PingopsSpanProcessor = class {
182
183
  });
183
184
  try {
184
185
  if (!(0, _pingops_core.isSpanEligible)(span)) {
185
- logger$1.debug("Span not eligible, skipping", {
186
+ logger$2.debug("Span not eligible, skipping", {
186
187
  spanName: span.name,
187
188
  spanId: spanContext.spanId,
188
189
  reason: "not CLIENT or missing HTTP/GenAI attributes"
@@ -191,7 +192,7 @@ var PingopsSpanProcessor = class {
191
192
  }
192
193
  const attributes = span.attributes;
193
194
  const url$1 = (0, _pingops_core.getHttpUrlFromAttributes)(attributes) ?? "";
194
- logger$1.debug("Extracted URL for domain filtering", {
195
+ logger$2.debug("Extracted URL for domain filtering", {
195
196
  spanName: span.name,
196
197
  url: url$1,
197
198
  hasHttpUrl: !!attributes["http.url"],
@@ -200,17 +201,17 @@ var PingopsSpanProcessor = class {
200
201
  });
201
202
  if (url$1) {
202
203
  if (!(0, _pingops_core.shouldCaptureSpan)(url$1, this.config.domainAllowList, this.config.domainDenyList)) {
203
- logger$1.info("Span filtered out by domain rules", {
204
+ logger$2.info("Span filtered out by domain rules", {
204
205
  spanName: span.name,
205
206
  spanId: spanContext.spanId,
206
207
  url: url$1
207
208
  });
208
209
  return;
209
210
  }
210
- } else logger$1.debug("No URL found for domain filtering, proceeding", { spanName: span.name });
211
+ } else logger$2.debug("No URL found for domain filtering, proceeding", { spanName: span.name });
211
212
  const filteredSpan = createFilteredSpan(span, this.config.domainAllowList, this.config.headersAllowList, this.config.headersDenyList, this.config.captureRequestBody, this.config.captureResponseBody, this.config.headerRedaction);
212
213
  this.processor.onEnd(filteredSpan);
213
- logger$1.info("Span passed all filters and queued for export", {
214
+ logger$2.info("Span passed all filters and queued for export", {
214
215
  spanName: span.name,
215
216
  spanId: spanContext.spanId,
216
217
  traceId: spanContext.traceId,
@@ -218,7 +219,7 @@ var PingopsSpanProcessor = class {
218
219
  hasHeaderFiltering: !!(this.config.headersAllowList || this.config.headersDenyList)
219
220
  });
220
221
  } catch (error) {
221
- logger$1.error("Error processing span", {
222
+ logger$2.error("Error processing span", {
222
223
  spanName: span.name,
223
224
  spanId: spanContext.spanId,
224
225
  error: error instanceof Error ? error.message : String(error)
@@ -231,12 +232,12 @@ var PingopsSpanProcessor = class {
231
232
  * @returns Promise that resolves when all pending operations are complete
232
233
  */
233
234
  async forceFlush() {
234
- logger$1.info("Force flushing spans");
235
+ logger$2.info("Force flushing spans");
235
236
  try {
236
237
  await this.processor.forceFlush();
237
- logger$1.info("Force flush complete");
238
+ logger$2.info("Force flush complete");
238
239
  } catch (error) {
239
- logger$1.error("Error during force flush", { error: error instanceof Error ? error.message : String(error) });
240
+ logger$2.error("Error during force flush", { error: error instanceof Error ? error.message : String(error) });
240
241
  throw error;
241
242
  }
242
243
  }
@@ -246,12 +247,12 @@ var PingopsSpanProcessor = class {
246
247
  * @returns Promise that resolves when shutdown is complete
247
248
  */
248
249
  async shutdown() {
249
- logger$1.info("Shutting down processor");
250
+ logger$2.info("Shutting down processor");
250
251
  try {
251
252
  await this.processor.shutdown();
252
- logger$1.info("Processor shutdown complete");
253
+ logger$2.info("Processor shutdown complete");
253
254
  } catch (error) {
254
- logger$1.error("Error during processor shutdown", { error: error instanceof Error ? error.message : String(error) });
255
+ logger$2.error("Error during processor shutdown", { error: error instanceof Error ? error.message : String(error) });
255
256
  throw error;
256
257
  }
257
258
  }
@@ -266,7 +267,7 @@ const PINGOPS_GLOBAL_SYMBOL = Symbol.for("pingops");
266
267
  /**
267
268
  * Logger instance for tracer provider
268
269
  */
269
- const logger = (0, _pingops_core.createLogger)("[PingOps TracerProvider]");
270
+ const logger$1 = (0, _pingops_core.createLogger)("[PingOps TracerProvider]");
270
271
  /**
271
272
  * Creates initial global state
272
273
  */
@@ -281,21 +282,21 @@ function getGlobalState() {
281
282
  try {
282
283
  const g = globalThis;
283
284
  if (typeof g !== "object" || g === null) {
284
- logger.warn("globalThis is not available, using fallback state");
285
+ logger$1.warn("globalThis is not available, using fallback state");
285
286
  return initialState;
286
287
  }
287
288
  if (!g[PINGOPS_GLOBAL_SYMBOL]) {
288
- logger.debug("Creating new global state");
289
+ logger$1.debug("Creating new global state");
289
290
  Object.defineProperty(g, PINGOPS_GLOBAL_SYMBOL, {
290
291
  value: initialState,
291
292
  writable: false,
292
293
  configurable: false,
293
294
  enumerable: false
294
295
  });
295
- } else logger.debug("Retrieved existing global state");
296
+ } else logger$1.debug("Retrieved existing global state");
296
297
  return g[PINGOPS_GLOBAL_SYMBOL];
297
298
  } catch (err) {
298
- logger.error("Failed to access global state:", err instanceof Error ? err.message : String(err));
299
+ logger$1.error("Failed to access global state:", err instanceof Error ? err.message : String(err));
299
300
  return initialState;
300
301
  }
301
302
  }
@@ -313,11 +314,11 @@ function setPingopsTracerProvider(provider) {
313
314
  const state = getGlobalState();
314
315
  const hadProvider = state.isolatedTracerProvider !== null;
315
316
  state.isolatedTracerProvider = provider;
316
- if (provider) logger.info("Set isolated TracerProvider", {
317
+ if (provider) logger$1.info("Set isolated TracerProvider", {
317
318
  hadPrevious: hadProvider,
318
319
  providerType: provider.constructor.name
319
320
  });
320
- else logger.info("Cleared isolated TracerProvider", { hadPrevious: hadProvider });
321
+ else logger$1.info("Cleared isolated TracerProvider", { hadPrevious: hadProvider });
321
322
  }
322
323
  /**
323
324
  * Gets the TracerProvider for PingOps tracing operations.
@@ -331,31 +332,65 @@ function setPingopsTracerProvider(provider) {
331
332
  function getPingopsTracerProvider() {
332
333
  const { isolatedTracerProvider } = getGlobalState();
333
334
  if (isolatedTracerProvider) {
334
- logger.debug("Using isolated TracerProvider", { providerType: isolatedTracerProvider.constructor.name });
335
+ logger$1.debug("Using isolated TracerProvider", { providerType: isolatedTracerProvider.constructor.name });
335
336
  return isolatedTracerProvider;
336
337
  }
337
338
  const globalProvider = _opentelemetry_api.trace.getTracerProvider();
338
- logger.debug("Using global TracerProvider", { providerType: globalProvider.constructor.name });
339
+ logger$1.debug("Using global TracerProvider", { providerType: globalProvider.constructor.name });
339
340
  return globalProvider;
340
341
  }
341
342
  /**
342
343
  * Shuts down the TracerProvider and flushes remaining spans
343
344
  */
344
345
  async function shutdownTracerProvider() {
345
- logger.info("Shutting down TracerProvider");
346
+ logger$1.info("Shutting down TracerProvider");
346
347
  const providerWithShutdown = getPingopsTracerProvider();
347
348
  if (providerWithShutdown && typeof providerWithShutdown.shutdown === "function") {
348
- logger.debug("Calling provider.shutdown()");
349
+ logger$1.debug("Calling provider.shutdown()");
349
350
  try {
350
351
  await providerWithShutdown.shutdown();
351
- logger.info("TracerProvider shutdown complete");
352
+ logger$1.info("TracerProvider shutdown complete");
352
353
  } catch (error) {
353
- logger.error("Error during TracerProvider shutdown:", error instanceof Error ? error.message : String(error));
354
+ logger$1.error("Error during TracerProvider shutdown:", error instanceof Error ? error.message : String(error));
354
355
  throw error;
355
356
  }
356
- } else logger.warn("TracerProvider does not have shutdown method, skipping");
357
+ } else logger$1.warn("TracerProvider does not have shutdown method, skipping");
357
358
  setPingopsTracerProvider(null);
358
- logger.info("TracerProvider shutdown finished");
359
+ logger$1.info("TracerProvider shutdown finished");
360
+ }
361
+
362
+ //#endregion
363
+ //#region src/instrumentations/suppression-guard.ts
364
+ const logger = (0, _pingops_core.createLogger)("[PingOps SuppressionGuard]");
365
+ let hasLoggedSuppressionLeakWarning = false;
366
+ function normalizeUrl(url$1) {
367
+ try {
368
+ return new URL(url$1).toString();
369
+ } catch {
370
+ return null;
371
+ }
372
+ }
373
+ function isExporterRequestUrl(requestUrl) {
374
+ if (!requestUrl) return false;
375
+ const exporterUrl = getGlobalConfig()?.exportTraceUrl;
376
+ 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);
381
+ }
382
+ /**
383
+ * Returns a context for outbound span creation that neutralizes leaked suppression
384
+ * for user traffic while preserving suppression for exporter requests.
385
+ */
386
+ function resolveOutboundSpanParentContext(activeContext, requestUrl) {
387
+ if (!(0, _opentelemetry_core.isTracingSuppressed)(activeContext)) return activeContext;
388
+ if (isExporterRequestUrl(requestUrl)) return activeContext;
389
+ if (!hasLoggedSuppressionLeakWarning) {
390
+ logger.warn("Detected suppressed context for outbound user request; running instrumentation on ROOT_CONTEXT to prevent Noop spans from suppression leakage");
391
+ hasLoggedSuppressionLeakWarning = true;
392
+ } else logger.debug("Suppressed context detected for outbound user request; using ROOT_CONTEXT");
393
+ return _opentelemetry_api.ROOT_CONTEXT;
359
394
  }
360
395
 
361
396
  //#endregion
@@ -525,6 +560,20 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
525
560
  constructor(config) {
526
561
  super(config);
527
562
  this._config = this._createConfig(config);
563
+ this._installOutgoingSuppressionGuard();
564
+ }
565
+ /**
566
+ * HttpInstrumentation's span creation is private, so we wrap the instance method
567
+ * to swap suppressed parent contexts with ROOT_CONTEXT for outgoing user requests.
568
+ */
569
+ _installOutgoingSuppressionGuard() {
570
+ const target = this;
571
+ if (typeof target._startHttpSpan !== "function") return;
572
+ const originalStartHttpSpan = target._startHttpSpan.bind(this);
573
+ target._startHttpSpan = (name, options, ctx = _opentelemetry_api.context.active()) => {
574
+ 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));
576
+ };
528
577
  }
529
578
  _createConfig(config) {
530
579
  return {
@@ -782,16 +831,16 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
782
831
  if (hookAttributes) Object.entries(hookAttributes).forEach(([key, val]) => {
783
832
  attributes[key] = val;
784
833
  });
785
- const activeCtx = _opentelemetry_api.context.active();
786
- const currentSpan = _opentelemetry_api.trace.getSpan(activeCtx);
834
+ const spanParentContext = resolveOutboundSpanParentContext(_opentelemetry_api.context.active(), requestUrl.toString());
835
+ const currentSpan = _opentelemetry_api.trace.getSpan(spanParentContext);
787
836
  let span;
788
837
  if (config.requireParentforSpans && (!currentSpan || !_opentelemetry_api.trace.isSpanContextValid(currentSpan.spanContext()))) span = _opentelemetry_api.trace.wrapSpanContext(_opentelemetry_api.INVALID_SPAN_CONTEXT);
789
838
  else span = this.tracer.startSpan(requestMethod === "_OTHER" ? "HTTP" : requestMethod, {
790
839
  kind: _opentelemetry_api.SpanKind.CLIENT,
791
840
  attributes
792
- }, activeCtx);
841
+ }, spanParentContext);
793
842
  (0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => config.requestHook?.(span, request), (e) => e && this._diag.error("caught requestHook error: ", e), true);
794
- const requestContext = _opentelemetry_api.trace.setSpan(_opentelemetry_api.context.active(), span);
843
+ const requestContext = _opentelemetry_api.trace.setSpan(spanParentContext, span);
795
844
  const addedHeaders = {};
796
845
  _opentelemetry_api.propagation.inject(requestContext, addedHeaders);
797
846
  const headerEntries = Object.entries(addedHeaders);