@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.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { BatchSpanProcessor, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
2
2
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
3
3
|
import { HTTP_RESPONSE_CONTENT_ENCODING, PINGOPS_CAPTURE_REQUEST_BODY, PINGOPS_CAPTURE_RESPONSE_BODY, bufferToBodyString, createLogger, extractSpanPayload, getHttpUrlFromAttributes, getPropagatedAttributesFromContext, isCompressedContentEncoding, isSpanEligible, shouldCaptureSpan } from "@pingops/core";
|
|
4
|
-
import { INVALID_SPAN_CONTEXT, SpanKind, SpanStatusCode, ValueType, context, propagation, trace } from "@opentelemetry/api";
|
|
4
|
+
import { INVALID_SPAN_CONTEXT, ROOT_CONTEXT, SpanKind, SpanStatusCode, ValueType, context, propagation, trace } from "@opentelemetry/api";
|
|
5
5
|
import "@opentelemetry/sdk-trace-node";
|
|
6
6
|
import "@opentelemetry/resources";
|
|
7
7
|
import { ATTR_ERROR_TYPE, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_REQUEST_METHOD_ORIGINAL, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_NETWORK_PEER_ADDRESS, ATTR_NETWORK_PEER_PORT, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_URL_FULL, ATTR_URL_PATH, ATTR_URL_QUERY, ATTR_URL_SCHEME, ATTR_USER_AGENT_ORIGINAL, METRIC_HTTP_CLIENT_REQUEST_DURATION } from "@opentelemetry/semantic-conventions";
|
|
8
8
|
import { ClientRequest, IncomingMessage } from "http";
|
|
9
9
|
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
|
|
10
|
+
import { hrTime, hrTimeDuration, hrTimeToMilliseconds, isTracingSuppressed } from "@opentelemetry/core";
|
|
10
11
|
import * as diagch from "diagnostics_channel";
|
|
11
12
|
import { URL as URL$1 } from "url";
|
|
12
13
|
import { InstrumentationBase, safeExecuteInTheMiddle } from "@opentelemetry/instrumentation";
|
|
13
|
-
import { hrTime, hrTimeDuration, hrTimeToMilliseconds } from "@opentelemetry/core";
|
|
14
14
|
|
|
15
15
|
//#region src/config-store.ts
|
|
16
16
|
let globalConfig = null;
|
|
@@ -31,7 +31,7 @@ function getGlobalConfig() {
|
|
|
31
31
|
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region src/span-processor.ts
|
|
34
|
-
const logger$
|
|
34
|
+
const logger$2 = createLogger("[PingOps Processor]");
|
|
35
35
|
/**
|
|
36
36
|
* Creates a filtered span wrapper that applies header filtering to attributes
|
|
37
37
|
*
|
|
@@ -49,7 +49,7 @@ const logger$1 = createLogger("[PingOps Processor]");
|
|
|
49
49
|
function createFilteredSpan(span, domainAllowList, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction) {
|
|
50
50
|
const payload = extractSpanPayload(span, domainAllowList, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction);
|
|
51
51
|
const filteredAttributes = payload?.attributes ?? span.attributes;
|
|
52
|
-
logger$
|
|
52
|
+
logger$2.debug("Payload", { payload });
|
|
53
53
|
return new Proxy(span, { get(target, prop) {
|
|
54
54
|
if (prop === "attributes") return filteredAttributes;
|
|
55
55
|
const value = target[prop];
|
|
@@ -102,9 +102,10 @@ var PingopsSpanProcessor = class {
|
|
|
102
102
|
captureResponseBody: config.captureResponseBody,
|
|
103
103
|
domainAllowList: config.domainAllowList,
|
|
104
104
|
maxRequestBodySize: config.maxRequestBodySize,
|
|
105
|
-
maxResponseBodySize: config.maxResponseBodySize
|
|
105
|
+
maxResponseBodySize: config.maxResponseBodySize,
|
|
106
|
+
exportTraceUrl: `${config.baseUrl}/v1/traces`
|
|
106
107
|
});
|
|
107
|
-
logger$
|
|
108
|
+
logger$2.info("Initialized PingopsSpanProcessor", {
|
|
108
109
|
baseUrl: config.baseUrl,
|
|
109
110
|
exportMode,
|
|
110
111
|
batchSize: config.batchSize,
|
|
@@ -120,7 +121,7 @@ var PingopsSpanProcessor = class {
|
|
|
120
121
|
*/
|
|
121
122
|
onStart(span, parentContext) {
|
|
122
123
|
const spanContext = span.spanContext();
|
|
123
|
-
logger$
|
|
124
|
+
logger$2.debug("Span started", {
|
|
124
125
|
spanName: span.name,
|
|
125
126
|
spanId: spanContext.spanId,
|
|
126
127
|
traceId: spanContext.traceId
|
|
@@ -128,7 +129,7 @@ var PingopsSpanProcessor = class {
|
|
|
128
129
|
const propagatedAttributes = getPropagatedAttributesFromContext(parentContext);
|
|
129
130
|
if (Object.keys(propagatedAttributes).length > 0) {
|
|
130
131
|
for (const [key, value] of Object.entries(propagatedAttributes)) if (typeof value === "string" || Array.isArray(value)) span.setAttribute(key, value);
|
|
131
|
-
logger$
|
|
132
|
+
logger$2.debug("Set propagated attributes on span", {
|
|
132
133
|
spanName: span.name,
|
|
133
134
|
attributeKeys: Object.keys(propagatedAttributes)
|
|
134
135
|
});
|
|
@@ -146,7 +147,7 @@ var PingopsSpanProcessor = class {
|
|
|
146
147
|
*/
|
|
147
148
|
onEnd(span) {
|
|
148
149
|
const spanContext = span.spanContext();
|
|
149
|
-
logger$
|
|
150
|
+
logger$2.debug("Span ended, processing", {
|
|
150
151
|
spanName: span.name,
|
|
151
152
|
spanId: spanContext.spanId,
|
|
152
153
|
traceId: spanContext.traceId,
|
|
@@ -154,7 +155,7 @@ var PingopsSpanProcessor = class {
|
|
|
154
155
|
});
|
|
155
156
|
try {
|
|
156
157
|
if (!isSpanEligible(span)) {
|
|
157
|
-
logger$
|
|
158
|
+
logger$2.debug("Span not eligible, skipping", {
|
|
158
159
|
spanName: span.name,
|
|
159
160
|
spanId: spanContext.spanId,
|
|
160
161
|
reason: "not CLIENT or missing HTTP/GenAI attributes"
|
|
@@ -163,7 +164,7 @@ var PingopsSpanProcessor = class {
|
|
|
163
164
|
}
|
|
164
165
|
const attributes = span.attributes;
|
|
165
166
|
const url = getHttpUrlFromAttributes(attributes) ?? "";
|
|
166
|
-
logger$
|
|
167
|
+
logger$2.debug("Extracted URL for domain filtering", {
|
|
167
168
|
spanName: span.name,
|
|
168
169
|
url,
|
|
169
170
|
hasHttpUrl: !!attributes["http.url"],
|
|
@@ -172,17 +173,17 @@ var PingopsSpanProcessor = class {
|
|
|
172
173
|
});
|
|
173
174
|
if (url) {
|
|
174
175
|
if (!shouldCaptureSpan(url, this.config.domainAllowList, this.config.domainDenyList)) {
|
|
175
|
-
logger$
|
|
176
|
+
logger$2.info("Span filtered out by domain rules", {
|
|
176
177
|
spanName: span.name,
|
|
177
178
|
spanId: spanContext.spanId,
|
|
178
179
|
url
|
|
179
180
|
});
|
|
180
181
|
return;
|
|
181
182
|
}
|
|
182
|
-
} else logger$
|
|
183
|
+
} else logger$2.debug("No URL found for domain filtering, proceeding", { spanName: span.name });
|
|
183
184
|
const filteredSpan = createFilteredSpan(span, this.config.domainAllowList, this.config.headersAllowList, this.config.headersDenyList, this.config.captureRequestBody, this.config.captureResponseBody, this.config.headerRedaction);
|
|
184
185
|
this.processor.onEnd(filteredSpan);
|
|
185
|
-
logger$
|
|
186
|
+
logger$2.info("Span passed all filters and queued for export", {
|
|
186
187
|
spanName: span.name,
|
|
187
188
|
spanId: spanContext.spanId,
|
|
188
189
|
traceId: spanContext.traceId,
|
|
@@ -190,7 +191,7 @@ var PingopsSpanProcessor = class {
|
|
|
190
191
|
hasHeaderFiltering: !!(this.config.headersAllowList || this.config.headersDenyList)
|
|
191
192
|
});
|
|
192
193
|
} catch (error) {
|
|
193
|
-
logger$
|
|
194
|
+
logger$2.error("Error processing span", {
|
|
194
195
|
spanName: span.name,
|
|
195
196
|
spanId: spanContext.spanId,
|
|
196
197
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -203,12 +204,12 @@ var PingopsSpanProcessor = class {
|
|
|
203
204
|
* @returns Promise that resolves when all pending operations are complete
|
|
204
205
|
*/
|
|
205
206
|
async forceFlush() {
|
|
206
|
-
logger$
|
|
207
|
+
logger$2.info("Force flushing spans");
|
|
207
208
|
try {
|
|
208
209
|
await this.processor.forceFlush();
|
|
209
|
-
logger$
|
|
210
|
+
logger$2.info("Force flush complete");
|
|
210
211
|
} catch (error) {
|
|
211
|
-
logger$
|
|
212
|
+
logger$2.error("Error during force flush", { error: error instanceof Error ? error.message : String(error) });
|
|
212
213
|
throw error;
|
|
213
214
|
}
|
|
214
215
|
}
|
|
@@ -218,12 +219,12 @@ var PingopsSpanProcessor = class {
|
|
|
218
219
|
* @returns Promise that resolves when shutdown is complete
|
|
219
220
|
*/
|
|
220
221
|
async shutdown() {
|
|
221
|
-
logger$
|
|
222
|
+
logger$2.info("Shutting down processor");
|
|
222
223
|
try {
|
|
223
224
|
await this.processor.shutdown();
|
|
224
|
-
logger$
|
|
225
|
+
logger$2.info("Processor shutdown complete");
|
|
225
226
|
} catch (error) {
|
|
226
|
-
logger$
|
|
227
|
+
logger$2.error("Error during processor shutdown", { error: error instanceof Error ? error.message : String(error) });
|
|
227
228
|
throw error;
|
|
228
229
|
}
|
|
229
230
|
}
|
|
@@ -238,7 +239,7 @@ const PINGOPS_GLOBAL_SYMBOL = Symbol.for("pingops");
|
|
|
238
239
|
/**
|
|
239
240
|
* Logger instance for tracer provider
|
|
240
241
|
*/
|
|
241
|
-
const logger = createLogger("[PingOps TracerProvider]");
|
|
242
|
+
const logger$1 = createLogger("[PingOps TracerProvider]");
|
|
242
243
|
/**
|
|
243
244
|
* Creates initial global state
|
|
244
245
|
*/
|
|
@@ -253,21 +254,21 @@ function getGlobalState() {
|
|
|
253
254
|
try {
|
|
254
255
|
const g = globalThis;
|
|
255
256
|
if (typeof g !== "object" || g === null) {
|
|
256
|
-
logger.warn("globalThis is not available, using fallback state");
|
|
257
|
+
logger$1.warn("globalThis is not available, using fallback state");
|
|
257
258
|
return initialState;
|
|
258
259
|
}
|
|
259
260
|
if (!g[PINGOPS_GLOBAL_SYMBOL]) {
|
|
260
|
-
logger.debug("Creating new global state");
|
|
261
|
+
logger$1.debug("Creating new global state");
|
|
261
262
|
Object.defineProperty(g, PINGOPS_GLOBAL_SYMBOL, {
|
|
262
263
|
value: initialState,
|
|
263
264
|
writable: false,
|
|
264
265
|
configurable: false,
|
|
265
266
|
enumerable: false
|
|
266
267
|
});
|
|
267
|
-
} else logger.debug("Retrieved existing global state");
|
|
268
|
+
} else logger$1.debug("Retrieved existing global state");
|
|
268
269
|
return g[PINGOPS_GLOBAL_SYMBOL];
|
|
269
270
|
} catch (err) {
|
|
270
|
-
logger.error("Failed to access global state:", err instanceof Error ? err.message : String(err));
|
|
271
|
+
logger$1.error("Failed to access global state:", err instanceof Error ? err.message : String(err));
|
|
271
272
|
return initialState;
|
|
272
273
|
}
|
|
273
274
|
}
|
|
@@ -285,11 +286,11 @@ function setPingopsTracerProvider(provider) {
|
|
|
285
286
|
const state = getGlobalState();
|
|
286
287
|
const hadProvider = state.isolatedTracerProvider !== null;
|
|
287
288
|
state.isolatedTracerProvider = provider;
|
|
288
|
-
if (provider) logger.info("Set isolated TracerProvider", {
|
|
289
|
+
if (provider) logger$1.info("Set isolated TracerProvider", {
|
|
289
290
|
hadPrevious: hadProvider,
|
|
290
291
|
providerType: provider.constructor.name
|
|
291
292
|
});
|
|
292
|
-
else logger.info("Cleared isolated TracerProvider", { hadPrevious: hadProvider });
|
|
293
|
+
else logger$1.info("Cleared isolated TracerProvider", { hadPrevious: hadProvider });
|
|
293
294
|
}
|
|
294
295
|
/**
|
|
295
296
|
* Gets the TracerProvider for PingOps tracing operations.
|
|
@@ -303,31 +304,65 @@ function setPingopsTracerProvider(provider) {
|
|
|
303
304
|
function getPingopsTracerProvider() {
|
|
304
305
|
const { isolatedTracerProvider } = getGlobalState();
|
|
305
306
|
if (isolatedTracerProvider) {
|
|
306
|
-
logger.debug("Using isolated TracerProvider", { providerType: isolatedTracerProvider.constructor.name });
|
|
307
|
+
logger$1.debug("Using isolated TracerProvider", { providerType: isolatedTracerProvider.constructor.name });
|
|
307
308
|
return isolatedTracerProvider;
|
|
308
309
|
}
|
|
309
310
|
const globalProvider = trace.getTracerProvider();
|
|
310
|
-
logger.debug("Using global TracerProvider", { providerType: globalProvider.constructor.name });
|
|
311
|
+
logger$1.debug("Using global TracerProvider", { providerType: globalProvider.constructor.name });
|
|
311
312
|
return globalProvider;
|
|
312
313
|
}
|
|
313
314
|
/**
|
|
314
315
|
* Shuts down the TracerProvider and flushes remaining spans
|
|
315
316
|
*/
|
|
316
317
|
async function shutdownTracerProvider() {
|
|
317
|
-
logger.info("Shutting down TracerProvider");
|
|
318
|
+
logger$1.info("Shutting down TracerProvider");
|
|
318
319
|
const providerWithShutdown = getPingopsTracerProvider();
|
|
319
320
|
if (providerWithShutdown && typeof providerWithShutdown.shutdown === "function") {
|
|
320
|
-
logger.debug("Calling provider.shutdown()");
|
|
321
|
+
logger$1.debug("Calling provider.shutdown()");
|
|
321
322
|
try {
|
|
322
323
|
await providerWithShutdown.shutdown();
|
|
323
|
-
logger.info("TracerProvider shutdown complete");
|
|
324
|
+
logger$1.info("TracerProvider shutdown complete");
|
|
324
325
|
} catch (error) {
|
|
325
|
-
logger.error("Error during TracerProvider shutdown:", error instanceof Error ? error.message : String(error));
|
|
326
|
+
logger$1.error("Error during TracerProvider shutdown:", error instanceof Error ? error.message : String(error));
|
|
326
327
|
throw error;
|
|
327
328
|
}
|
|
328
|
-
} else logger.warn("TracerProvider does not have shutdown method, skipping");
|
|
329
|
+
} else logger$1.warn("TracerProvider does not have shutdown method, skipping");
|
|
329
330
|
setPingopsTracerProvider(null);
|
|
330
|
-
logger.info("TracerProvider shutdown finished");
|
|
331
|
+
logger$1.info("TracerProvider shutdown finished");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/instrumentations/suppression-guard.ts
|
|
336
|
+
const logger = createLogger("[PingOps SuppressionGuard]");
|
|
337
|
+
let hasLoggedSuppressionLeakWarning = false;
|
|
338
|
+
function normalizeUrl(url) {
|
|
339
|
+
try {
|
|
340
|
+
return new URL(url).toString();
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function isExporterRequestUrl(requestUrl) {
|
|
346
|
+
if (!requestUrl) return false;
|
|
347
|
+
const exporterUrl = getGlobalConfig()?.exportTraceUrl;
|
|
348
|
+
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);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Returns a context for outbound span creation that neutralizes leaked suppression
|
|
356
|
+
* for user traffic while preserving suppression for exporter requests.
|
|
357
|
+
*/
|
|
358
|
+
function resolveOutboundSpanParentContext(activeContext, requestUrl) {
|
|
359
|
+
if (!isTracingSuppressed(activeContext)) return activeContext;
|
|
360
|
+
if (isExporterRequestUrl(requestUrl)) return activeContext;
|
|
361
|
+
if (!hasLoggedSuppressionLeakWarning) {
|
|
362
|
+
logger.warn("Detected suppressed context for outbound user request; running instrumentation on ROOT_CONTEXT to prevent Noop spans from suppression leakage");
|
|
363
|
+
hasLoggedSuppressionLeakWarning = true;
|
|
364
|
+
} else logger.debug("Suppressed context detected for outbound user request; using ROOT_CONTEXT");
|
|
365
|
+
return ROOT_CONTEXT;
|
|
331
366
|
}
|
|
332
367
|
|
|
333
368
|
//#endregion
|
|
@@ -497,6 +532,20 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
|
|
|
497
532
|
constructor(config) {
|
|
498
533
|
super(config);
|
|
499
534
|
this._config = this._createConfig(config);
|
|
535
|
+
this._installOutgoingSuppressionGuard();
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* HttpInstrumentation's span creation is private, so we wrap the instance method
|
|
539
|
+
* to swap suppressed parent contexts with ROOT_CONTEXT for outgoing user requests.
|
|
540
|
+
*/
|
|
541
|
+
_installOutgoingSuppressionGuard() {
|
|
542
|
+
const target = this;
|
|
543
|
+
if (typeof target._startHttpSpan !== "function") return;
|
|
544
|
+
const originalStartHttpSpan = target._startHttpSpan.bind(this);
|
|
545
|
+
target._startHttpSpan = (name, options, ctx = context.active()) => {
|
|
546
|
+
if (options.kind !== SpanKind.CLIENT) return originalStartHttpSpan(name, options, ctx);
|
|
547
|
+
return originalStartHttpSpan(name, options, resolveOutboundSpanParentContext(ctx, typeof options.attributes?.["url.full"] === "string" ? options.attributes["url.full"] : void 0));
|
|
548
|
+
};
|
|
500
549
|
}
|
|
501
550
|
_createConfig(config) {
|
|
502
551
|
return {
|
|
@@ -754,16 +803,16 @@ var UndiciInstrumentation = class extends InstrumentationBase {
|
|
|
754
803
|
if (hookAttributes) Object.entries(hookAttributes).forEach(([key, val]) => {
|
|
755
804
|
attributes[key] = val;
|
|
756
805
|
});
|
|
757
|
-
const
|
|
758
|
-
const currentSpan = trace.getSpan(
|
|
806
|
+
const spanParentContext = resolveOutboundSpanParentContext(context.active(), requestUrl.toString());
|
|
807
|
+
const currentSpan = trace.getSpan(spanParentContext);
|
|
759
808
|
let span;
|
|
760
809
|
if (config.requireParentforSpans && (!currentSpan || !trace.isSpanContextValid(currentSpan.spanContext()))) span = trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
|
|
761
810
|
else span = this.tracer.startSpan(requestMethod === "_OTHER" ? "HTTP" : requestMethod, {
|
|
762
811
|
kind: SpanKind.CLIENT,
|
|
763
812
|
attributes
|
|
764
|
-
},
|
|
813
|
+
}, spanParentContext);
|
|
765
814
|
safeExecuteInTheMiddle(() => config.requestHook?.(span, request), (e) => e && this._diag.error("caught requestHook error: ", e), true);
|
|
766
|
-
const requestContext = trace.setSpan(
|
|
815
|
+
const requestContext = trace.setSpan(spanParentContext, span);
|
|
767
816
|
const addedHeaders = {};
|
|
768
817
|
propagation.inject(requestContext, addedHeaders);
|
|
769
818
|
const headerEntries = Object.entries(addedHeaders);
|