@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 +88 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +5 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +89 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
235
|
+
logger$2.info("Force flushing spans");
|
|
235
236
|
try {
|
|
236
237
|
await this.processor.forceFlush();
|
|
237
|
-
logger$
|
|
238
|
+
logger$2.info("Force flush complete");
|
|
238
239
|
} catch (error) {
|
|
239
|
-
logger$
|
|
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$
|
|
250
|
+
logger$2.info("Shutting down processor");
|
|
250
251
|
try {
|
|
251
252
|
await this.processor.shutdown();
|
|
252
|
-
logger$
|
|
253
|
+
logger$2.info("Processor shutdown complete");
|
|
253
254
|
} catch (error) {
|
|
254
|
-
logger$
|
|
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
|
|
786
|
-
const currentSpan = _opentelemetry_api.trace.getSpan(
|
|
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
|
-
},
|
|
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(
|
|
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);
|