@pingops/otel 0.2.6 → 0.4.0
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/README.md +27 -4
- package/dist/index.cjs +645 -246
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -71
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +34 -71
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +646 -247
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,7 @@ let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conve
|
|
|
35
35
|
let http = require("http");
|
|
36
36
|
let _opentelemetry_instrumentation_http = require("@opentelemetry/instrumentation-http");
|
|
37
37
|
let _opentelemetry_core = require("@opentelemetry/core");
|
|
38
|
+
let zlib = require("zlib");
|
|
38
39
|
let diagnostics_channel = require("diagnostics_channel");
|
|
39
40
|
diagnostics_channel = __toESM(diagnostics_channel);
|
|
40
41
|
let url = require("url");
|
|
@@ -59,7 +60,7 @@ function getGlobalConfig() {
|
|
|
59
60
|
|
|
60
61
|
//#endregion
|
|
61
62
|
//#region src/span-processor.ts
|
|
62
|
-
const logger$
|
|
63
|
+
const logger$3 = (0, _pingops_core.createLogger)("[PingOps Processor]");
|
|
63
64
|
function normalizePath$1(pathname) {
|
|
64
65
|
return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
65
66
|
}
|
|
@@ -89,10 +90,10 @@ function isExporterRequestUrl$1(url$1, exporterUrl) {
|
|
|
89
90
|
*
|
|
90
91
|
* This allows us to filter headers before the span is serialized by OTLP exporter
|
|
91
92
|
*/
|
|
92
|
-
function createFilteredSpan(span, domainAllowList,
|
|
93
|
-
const payload = (0, _pingops_core.extractSpanPayload)(span, domainAllowList,
|
|
93
|
+
function createFilteredSpan(span, domainAllowList, globalCaptureRequestBody, globalCaptureResponseBody, transforms) {
|
|
94
|
+
const payload = (0, _pingops_core.extractSpanPayload)(span, domainAllowList, globalCaptureRequestBody, globalCaptureResponseBody, transforms);
|
|
94
95
|
const filteredAttributes = payload?.attributes ?? span.attributes;
|
|
95
|
-
logger$
|
|
96
|
+
logger$3.debug("Payload", { payload });
|
|
96
97
|
return new Proxy(span, { get(target, prop) {
|
|
97
98
|
if (prop === "attributes") return filteredAttributes;
|
|
98
99
|
const value = target[prop];
|
|
@@ -134,31 +135,28 @@ var PingopsSpanProcessor = class {
|
|
|
134
135
|
});
|
|
135
136
|
this.config = {
|
|
136
137
|
debug: config.debug ?? false,
|
|
137
|
-
|
|
138
|
-
headersDenyList: config.headersDenyList,
|
|
138
|
+
sdkVersion: config.sdkVersion,
|
|
139
139
|
domainAllowList: config.domainAllowList,
|
|
140
140
|
domainDenyList: config.domainDenyList,
|
|
141
141
|
captureRequestBody: config.captureRequestBody,
|
|
142
142
|
captureResponseBody: config.captureResponseBody,
|
|
143
|
-
|
|
143
|
+
transforms: config.transforms
|
|
144
144
|
};
|
|
145
145
|
setGlobalConfig({
|
|
146
146
|
captureRequestBody: config.captureRequestBody,
|
|
147
147
|
captureResponseBody: config.captureResponseBody,
|
|
148
148
|
domainAllowList: config.domainAllowList,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
exportTraceUrl: this.exporterTraceUrl
|
|
149
|
+
exportTraceUrl: this.exporterTraceUrl,
|
|
150
|
+
llmMonitoring: config.llmMonitoring
|
|
152
151
|
});
|
|
153
|
-
logger$
|
|
152
|
+
logger$3.info("Initialized PingopsSpanProcessor", {
|
|
154
153
|
baseUrl: config.baseUrl,
|
|
155
154
|
exportMode,
|
|
156
155
|
batchSize: config.batchSize,
|
|
157
156
|
batchTimeout: config.batchTimeout,
|
|
158
157
|
hasDomainAllowList: !!config.domainAllowList && config.domainAllowList.length > 0,
|
|
159
158
|
hasDomainDenyList: !!config.domainDenyList && config.domainDenyList.length > 0,
|
|
160
|
-
|
|
161
|
-
hasHeadersDenyList: !!config.headersDenyList && config.headersDenyList.length > 0
|
|
159
|
+
hasTransforms: !!config.transforms
|
|
162
160
|
});
|
|
163
161
|
}
|
|
164
162
|
/**
|
|
@@ -166,15 +164,16 @@ var PingopsSpanProcessor = class {
|
|
|
166
164
|
*/
|
|
167
165
|
onStart(span, parentContext) {
|
|
168
166
|
const spanContext = span.spanContext();
|
|
169
|
-
logger$
|
|
167
|
+
logger$3.debug("Span started", {
|
|
170
168
|
spanName: span.name,
|
|
171
169
|
spanId: spanContext.spanId,
|
|
172
170
|
traceId: spanContext.traceId
|
|
173
171
|
});
|
|
172
|
+
if (this.config.sdkVersion) span.setAttribute("pingops.sdk.version", this.config.sdkVersion);
|
|
174
173
|
const propagatedAttributes = (0, _pingops_core.getPropagatedAttributesFromContext)(parentContext);
|
|
175
174
|
if (Object.keys(propagatedAttributes).length > 0) {
|
|
176
175
|
for (const [key, value] of Object.entries(propagatedAttributes)) if (typeof value === "string" || Array.isArray(value)) span.setAttribute(key, value);
|
|
177
|
-
logger$
|
|
176
|
+
logger$3.debug("Set propagated attributes on span", {
|
|
178
177
|
spanName: span.name,
|
|
179
178
|
attributeKeys: Object.keys(propagatedAttributes)
|
|
180
179
|
});
|
|
@@ -192,7 +191,7 @@ var PingopsSpanProcessor = class {
|
|
|
192
191
|
*/
|
|
193
192
|
onEnd(span) {
|
|
194
193
|
const spanContext = span.spanContext();
|
|
195
|
-
logger$
|
|
194
|
+
logger$3.debug("Span ended, processing", {
|
|
196
195
|
spanName: span.name,
|
|
197
196
|
spanId: spanContext.spanId,
|
|
198
197
|
traceId: spanContext.traceId,
|
|
@@ -200,7 +199,7 @@ var PingopsSpanProcessor = class {
|
|
|
200
199
|
});
|
|
201
200
|
try {
|
|
202
201
|
if (!(0, _pingops_core.isSpanEligible)(span)) {
|
|
203
|
-
logger$
|
|
202
|
+
logger$3.debug("Span not eligible, skipping", {
|
|
204
203
|
spanName: span.name,
|
|
205
204
|
spanId: spanContext.spanId,
|
|
206
205
|
reason: "not CLIENT or missing HTTP/GenAI attributes"
|
|
@@ -210,14 +209,14 @@ var PingopsSpanProcessor = class {
|
|
|
210
209
|
const attributes = span.attributes;
|
|
211
210
|
const url$1 = (0, _pingops_core.getHttpUrlFromAttributes)(attributes) ?? "";
|
|
212
211
|
if (url$1 && isExporterRequestUrl$1(url$1, this.exporterTraceUrl)) {
|
|
213
|
-
logger$
|
|
212
|
+
logger$3.debug("Skipping exporter span to prevent self-instrumentation", {
|
|
214
213
|
spanName: span.name,
|
|
215
214
|
spanId: spanContext.spanId,
|
|
216
215
|
url: url$1
|
|
217
216
|
});
|
|
218
217
|
return;
|
|
219
218
|
}
|
|
220
|
-
logger$
|
|
219
|
+
logger$3.debug("Extracted URL for domain filtering", {
|
|
221
220
|
spanName: span.name,
|
|
222
221
|
url: url$1,
|
|
223
222
|
hasHttpUrl: !!attributes["http.url"],
|
|
@@ -226,25 +225,25 @@ var PingopsSpanProcessor = class {
|
|
|
226
225
|
});
|
|
227
226
|
if (url$1) {
|
|
228
227
|
if (!(0, _pingops_core.shouldCaptureSpan)(url$1, this.config.domainAllowList, this.config.domainDenyList)) {
|
|
229
|
-
logger$
|
|
228
|
+
logger$3.info("Span filtered out by domain rules", {
|
|
230
229
|
spanName: span.name,
|
|
231
230
|
spanId: spanContext.spanId,
|
|
232
231
|
url: url$1
|
|
233
232
|
});
|
|
234
233
|
return;
|
|
235
234
|
}
|
|
236
|
-
} else logger$
|
|
237
|
-
const filteredSpan = createFilteredSpan(span, this.config.domainAllowList, this.config.
|
|
235
|
+
} else logger$3.debug("No URL found for domain filtering, proceeding", { spanName: span.name });
|
|
236
|
+
const filteredSpan = createFilteredSpan(span, this.config.domainAllowList, this.config.captureRequestBody, this.config.captureResponseBody, this.config.transforms);
|
|
238
237
|
this.processor.onEnd(filteredSpan);
|
|
239
|
-
logger$
|
|
238
|
+
logger$3.info("Span passed all filters and queued for export", {
|
|
240
239
|
spanName: span.name,
|
|
241
240
|
spanId: spanContext.spanId,
|
|
242
241
|
traceId: spanContext.traceId,
|
|
243
242
|
url: url$1,
|
|
244
|
-
|
|
243
|
+
hasTransforms: !!this.config.transforms
|
|
245
244
|
});
|
|
246
245
|
} catch (error) {
|
|
247
|
-
logger$
|
|
246
|
+
logger$3.error("Error processing span", {
|
|
248
247
|
spanName: span.name,
|
|
249
248
|
spanId: spanContext.spanId,
|
|
250
249
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -257,12 +256,12 @@ var PingopsSpanProcessor = class {
|
|
|
257
256
|
* @returns Promise that resolves when all pending operations are complete
|
|
258
257
|
*/
|
|
259
258
|
async forceFlush() {
|
|
260
|
-
logger$
|
|
259
|
+
logger$3.info("Force flushing spans");
|
|
261
260
|
try {
|
|
262
261
|
await this.processor.forceFlush();
|
|
263
|
-
logger$
|
|
262
|
+
logger$3.info("Force flush complete");
|
|
264
263
|
} catch (error) {
|
|
265
|
-
logger$
|
|
264
|
+
logger$3.error("Error during force flush", { error: error instanceof Error ? error.message : String(error) });
|
|
266
265
|
throw error;
|
|
267
266
|
}
|
|
268
267
|
}
|
|
@@ -272,12 +271,12 @@ var PingopsSpanProcessor = class {
|
|
|
272
271
|
* @returns Promise that resolves when shutdown is complete
|
|
273
272
|
*/
|
|
274
273
|
async shutdown() {
|
|
275
|
-
logger$
|
|
274
|
+
logger$3.info("Shutting down processor");
|
|
276
275
|
try {
|
|
277
276
|
await this.processor.shutdown();
|
|
278
|
-
logger$
|
|
277
|
+
logger$3.info("Processor shutdown complete");
|
|
279
278
|
} catch (error) {
|
|
280
|
-
logger$
|
|
279
|
+
logger$3.error("Error during processor shutdown", { error: error instanceof Error ? error.message : String(error) });
|
|
281
280
|
throw error;
|
|
282
281
|
}
|
|
283
282
|
}
|
|
@@ -292,7 +291,7 @@ const PINGOPS_GLOBAL_SYMBOL = Symbol.for("pingops");
|
|
|
292
291
|
/**
|
|
293
292
|
* Logger instance for tracer provider
|
|
294
293
|
*/
|
|
295
|
-
const logger$
|
|
294
|
+
const logger$2 = (0, _pingops_core.createLogger)("[PingOps TracerProvider]");
|
|
296
295
|
/**
|
|
297
296
|
* Creates initial global state
|
|
298
297
|
*/
|
|
@@ -307,21 +306,21 @@ function getGlobalState() {
|
|
|
307
306
|
try {
|
|
308
307
|
const g = globalThis;
|
|
309
308
|
if (typeof g !== "object" || g === null) {
|
|
310
|
-
logger$
|
|
309
|
+
logger$2.warn("globalThis is not available, using fallback state");
|
|
311
310
|
return initialState;
|
|
312
311
|
}
|
|
313
312
|
if (!g[PINGOPS_GLOBAL_SYMBOL]) {
|
|
314
|
-
logger$
|
|
313
|
+
logger$2.debug("Creating new global state");
|
|
315
314
|
Object.defineProperty(g, PINGOPS_GLOBAL_SYMBOL, {
|
|
316
315
|
value: initialState,
|
|
317
316
|
writable: false,
|
|
318
317
|
configurable: false,
|
|
319
318
|
enumerable: false
|
|
320
319
|
});
|
|
321
|
-
} else logger$
|
|
320
|
+
} else logger$2.debug("Retrieved existing global state");
|
|
322
321
|
return g[PINGOPS_GLOBAL_SYMBOL];
|
|
323
322
|
} catch (err) {
|
|
324
|
-
logger$
|
|
323
|
+
logger$2.error("Failed to access global state:", err instanceof Error ? err.message : String(err));
|
|
325
324
|
return initialState;
|
|
326
325
|
}
|
|
327
326
|
}
|
|
@@ -339,11 +338,11 @@ function setPingopsTracerProvider(provider) {
|
|
|
339
338
|
const state = getGlobalState();
|
|
340
339
|
const hadProvider = state.isolatedTracerProvider !== null;
|
|
341
340
|
state.isolatedTracerProvider = provider;
|
|
342
|
-
if (provider) logger$
|
|
341
|
+
if (provider) logger$2.info("Set isolated TracerProvider", {
|
|
343
342
|
hadPrevious: hadProvider,
|
|
344
343
|
providerType: provider.constructor.name
|
|
345
344
|
});
|
|
346
|
-
else logger$
|
|
345
|
+
else logger$2.info("Cleared isolated TracerProvider", { hadPrevious: hadProvider });
|
|
347
346
|
}
|
|
348
347
|
/**
|
|
349
348
|
* Gets the TracerProvider for PingOps tracing operations.
|
|
@@ -357,36 +356,36 @@ function setPingopsTracerProvider(provider) {
|
|
|
357
356
|
function getPingopsTracerProvider() {
|
|
358
357
|
const { isolatedTracerProvider } = getGlobalState();
|
|
359
358
|
if (isolatedTracerProvider) {
|
|
360
|
-
logger$
|
|
359
|
+
logger$2.debug("Using isolated TracerProvider", { providerType: isolatedTracerProvider.constructor.name });
|
|
361
360
|
return isolatedTracerProvider;
|
|
362
361
|
}
|
|
363
362
|
const globalProvider = _opentelemetry_api.trace.getTracerProvider();
|
|
364
|
-
logger$
|
|
363
|
+
logger$2.debug("Using global TracerProvider", { providerType: globalProvider.constructor.name });
|
|
365
364
|
return globalProvider;
|
|
366
365
|
}
|
|
367
366
|
/**
|
|
368
367
|
* Shuts down the TracerProvider and flushes remaining spans
|
|
369
368
|
*/
|
|
370
369
|
async function shutdownTracerProvider() {
|
|
371
|
-
logger$
|
|
370
|
+
logger$2.info("Shutting down TracerProvider");
|
|
372
371
|
const providerWithShutdown = getPingopsTracerProvider();
|
|
373
372
|
if (providerWithShutdown && typeof providerWithShutdown.shutdown === "function") {
|
|
374
|
-
logger$
|
|
373
|
+
logger$2.debug("Calling provider.shutdown()");
|
|
375
374
|
try {
|
|
376
375
|
await providerWithShutdown.shutdown();
|
|
377
|
-
logger$
|
|
376
|
+
logger$2.info("TracerProvider shutdown complete");
|
|
378
377
|
} catch (error) {
|
|
379
|
-
logger$
|
|
378
|
+
logger$2.error("Error during TracerProvider shutdown:", error instanceof Error ? error.message : String(error));
|
|
380
379
|
throw error;
|
|
381
380
|
}
|
|
382
|
-
} else logger$
|
|
381
|
+
} else logger$2.warn("TracerProvider does not have shutdown method, skipping");
|
|
383
382
|
setPingopsTracerProvider(null);
|
|
384
|
-
logger$
|
|
383
|
+
logger$2.info("TracerProvider shutdown finished");
|
|
385
384
|
}
|
|
386
385
|
|
|
387
386
|
//#endregion
|
|
388
387
|
//#region src/instrumentations/suppression-guard.ts
|
|
389
|
-
const logger = (0, _pingops_core.createLogger)("[PingOps SuppressionGuard]");
|
|
388
|
+
const logger$1 = (0, _pingops_core.createLogger)("[PingOps SuppressionGuard]");
|
|
390
389
|
let hasLoggedSuppressionLeakWarning = false;
|
|
391
390
|
function normalizePath(pathname) {
|
|
392
391
|
return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
@@ -423,26 +422,496 @@ function shouldIgnoreOutboundInstrumentation(requestUrl) {
|
|
|
423
422
|
*/
|
|
424
423
|
function resolveOutboundSpanParentContext(activeContext, requestUrl) {
|
|
425
424
|
if (!(0, _opentelemetry_core.isTracingSuppressed)(activeContext)) return activeContext;
|
|
425
|
+
if (activeContext.getValue(_pingops_core.PINGOPS_INTENTIONAL_SUPPRESSION) === true) return activeContext;
|
|
426
426
|
if (isExporterRequestUrl(requestUrl)) return activeContext;
|
|
427
427
|
if (!hasLoggedSuppressionLeakWarning) {
|
|
428
|
-
logger.warn("Detected suppressed context for outbound user request; running instrumentation on ROOT_CONTEXT to prevent Noop spans from suppression leakage");
|
|
428
|
+
logger$1.warn("Detected suppressed context for outbound user request; running instrumentation on ROOT_CONTEXT to prevent Noop spans from suppression leakage");
|
|
429
429
|
hasLoggedSuppressionLeakWarning = true;
|
|
430
|
-
} else logger.debug("Suppressed context detected for outbound user request; using ROOT_CONTEXT");
|
|
430
|
+
} else logger$1.debug("Suppressed context detected for outbound user request; using ROOT_CONTEXT");
|
|
431
431
|
return _opentelemetry_api.ROOT_CONTEXT;
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
//#endregion
|
|
435
|
+
//#region src/instrumentations/body-utils.ts
|
|
436
|
+
const HTTP_REQUEST_BODY = "http.request.body";
|
|
437
|
+
const HTTP_RESPONSE_BODY = "http.response.body";
|
|
438
|
+
const HTTP_REQUEST_BODY_SIZE = "http.request.body.size";
|
|
439
|
+
const HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";
|
|
440
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
441
|
+
const BINARY_CONTENT_TYPES = new Set([
|
|
442
|
+
"application/octet-stream",
|
|
443
|
+
"application/pdf",
|
|
444
|
+
"application/zip",
|
|
445
|
+
"application/gzip",
|
|
446
|
+
"application/x-gzip",
|
|
447
|
+
"image/jpeg",
|
|
448
|
+
"image/jpg",
|
|
449
|
+
"image/png",
|
|
450
|
+
"image/gif",
|
|
451
|
+
"image/webp",
|
|
452
|
+
"image/bmp",
|
|
453
|
+
"image/tiff",
|
|
454
|
+
"image/ico",
|
|
455
|
+
"audio/mpeg",
|
|
456
|
+
"audio/mp3",
|
|
457
|
+
"audio/wav",
|
|
458
|
+
"audio/ogg",
|
|
459
|
+
"audio/webm",
|
|
460
|
+
"video/mp4",
|
|
461
|
+
"video/webm",
|
|
462
|
+
"video/ogg",
|
|
463
|
+
"video/avi",
|
|
464
|
+
"video/mov"
|
|
465
|
+
]);
|
|
466
|
+
/**
|
|
467
|
+
* Gets domain rule configuration for a given URL.
|
|
468
|
+
*/
|
|
469
|
+
function getDomainRule(url$1, domainAllowList) {
|
|
470
|
+
if (!domainAllowList) return;
|
|
471
|
+
const domain = (0, _pingops_core.extractDomainFromUrl)(url$1);
|
|
472
|
+
for (const rule of domainAllowList) if (domain === rule.domain || domain.endsWith(`.${rule.domain}`) || domain === rule.domain.slice(1)) return rule;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Determines if request body should be captured based on priority:
|
|
476
|
+
* context > domain rule > global config > default (false).
|
|
477
|
+
*/
|
|
478
|
+
function shouldCaptureRequestBody(url$1) {
|
|
479
|
+
const contextValue = _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_CAPTURE_REQUEST_BODY);
|
|
480
|
+
if (contextValue !== void 0) return contextValue;
|
|
481
|
+
if (url$1) {
|
|
482
|
+
const domainRule = getDomainRule(url$1, getGlobalConfig()?.domainAllowList);
|
|
483
|
+
if (domainRule?.captureRequestBody !== void 0) return domainRule.captureRequestBody;
|
|
484
|
+
}
|
|
485
|
+
const globalConfig$1 = getGlobalConfig();
|
|
486
|
+
if (globalConfig$1?.captureRequestBody !== void 0) return globalConfig$1.captureRequestBody;
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Determines if response body should be captured based on priority:
|
|
491
|
+
* context > domain rule > global config > default (false).
|
|
492
|
+
*/
|
|
493
|
+
function shouldCaptureResponseBody(url$1) {
|
|
494
|
+
const contextValue = _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_CAPTURE_RESPONSE_BODY);
|
|
495
|
+
if (contextValue !== void 0) return contextValue;
|
|
496
|
+
if (url$1) {
|
|
497
|
+
const domainRule = getDomainRule(url$1, getGlobalConfig()?.domainAllowList);
|
|
498
|
+
if (domainRule?.captureResponseBody !== void 0) return domainRule.captureResponseBody;
|
|
499
|
+
}
|
|
500
|
+
const globalConfig$1 = getGlobalConfig();
|
|
501
|
+
if (globalConfig$1?.captureResponseBody !== void 0) return globalConfig$1.captureResponseBody;
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Normalizes supported HTTP chunk types into a Buffer.
|
|
506
|
+
*/
|
|
507
|
+
function toBufferChunk(data) {
|
|
508
|
+
if (typeof data === "string") return Buffer.from(data);
|
|
509
|
+
if (Buffer.isBuffer(data)) return data;
|
|
510
|
+
if (data instanceof Uint8Array) return Buffer.from(data);
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Parses a content-length value into a positive byte count.
|
|
515
|
+
*/
|
|
516
|
+
function parseContentLength(value) {
|
|
517
|
+
if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
518
|
+
if (typeof value === "string") {
|
|
519
|
+
const parsed = Number(value);
|
|
520
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
521
|
+
}
|
|
522
|
+
if (Array.isArray(value) && value.length > 0) return parseContentLength(value[0]);
|
|
523
|
+
}
|
|
524
|
+
function normalizeHeaderValue$1(v) {
|
|
525
|
+
if (typeof v === "string") {
|
|
526
|
+
const trimmed = v.trim();
|
|
527
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
528
|
+
}
|
|
529
|
+
if (Array.isArray(v) && v.length > 0) return normalizeHeaderValue$1(v[0]);
|
|
530
|
+
if (typeof v === "number" && Number.isFinite(v)) return String(v);
|
|
531
|
+
}
|
|
532
|
+
function parseContentTypeMainType(contentType) {
|
|
533
|
+
const normalized = normalizeHeaderValue$1(contentType);
|
|
534
|
+
if (!normalized) return;
|
|
535
|
+
return normalized.toLowerCase().split(";")[0]?.trim() || void 0;
|
|
536
|
+
}
|
|
537
|
+
function isUtf8(buffer) {
|
|
538
|
+
if (buffer.length === 0) return true;
|
|
539
|
+
try {
|
|
540
|
+
UTF8_DECODER.decode(buffer);
|
|
541
|
+
return true;
|
|
542
|
+
} catch {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Encodes HTTP body for span attributes:
|
|
548
|
+
* - compressed payloads => base64 (backend can decode)
|
|
549
|
+
* - known binary content-types => base64
|
|
550
|
+
* - known textual/utf8 payloads => utf8
|
|
551
|
+
*/
|
|
552
|
+
function encodeBodyBufferForSpan(buffer, headers) {
|
|
553
|
+
if (!buffer || buffer.length === 0) return null;
|
|
554
|
+
const contentEncoding = normalizeHeaderValue$1(headers?.["content-encoding"]);
|
|
555
|
+
if ((0, _pingops_core.isCompressedContentEncoding)(contentEncoding)) return {
|
|
556
|
+
content: buffer.toString("base64"),
|
|
557
|
+
contentEncoding: contentEncoding?.split(",")[0]?.trim().toLowerCase() || void 0
|
|
558
|
+
};
|
|
559
|
+
const contentType = parseContentTypeMainType(headers?.["content-type"]);
|
|
560
|
+
if (contentType && BINARY_CONTENT_TYPES.has(contentType)) return { content: buffer.toString("base64") };
|
|
561
|
+
if (isUtf8(buffer)) return { content: buffer.toString("utf8") };
|
|
562
|
+
return { content: buffer.toString("base64") };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
//#endregion
|
|
566
|
+
//#region src/llm/types.ts
|
|
567
|
+
const GEN_AI_ATTRS = {
|
|
568
|
+
SYSTEM: "gen_ai.system",
|
|
569
|
+
PROVIDER_NAME: "gen_ai.provider.name",
|
|
570
|
+
OPERATION_NAME: "gen_ai.operation.name",
|
|
571
|
+
REQUEST_MODEL: "gen_ai.request.model",
|
|
572
|
+
RESPONSE_MODEL: "gen_ai.response.model",
|
|
573
|
+
RESPONSE_ID: "gen_ai.response.id",
|
|
574
|
+
USAGE_INPUT_TOKENS: "gen_ai.usage.input_tokens",
|
|
575
|
+
USAGE_OUTPUT_TOKENS: "gen_ai.usage.output_tokens"
|
|
576
|
+
};
|
|
577
|
+
const PINGOPS_GEN_AI_ATTRS = {
|
|
578
|
+
TOTAL_TOKENS: "pingops.gen_ai.usage.total_tokens",
|
|
579
|
+
CACHE_READ_INPUT_TOKENS: "pingops.gen_ai.usage.cache_read_input_tokens",
|
|
580
|
+
CACHE_CREATION_INPUT_TOKENS: "pingops.gen_ai.usage.cache_creation_input_tokens",
|
|
581
|
+
CACHE_TOKENS: "pingops.gen_ai.usage.cache_tokens"
|
|
582
|
+
};
|
|
583
|
+
const DEFAULT_LLM_MONITORING_CONFIG = {
|
|
584
|
+
enabled: false,
|
|
585
|
+
streaming: true
|
|
586
|
+
};
|
|
587
|
+
function normalizeLlmMonitoringConfig(config) {
|
|
588
|
+
return {
|
|
589
|
+
enabled: config?.enabled ?? DEFAULT_LLM_MONITORING_CONFIG.enabled,
|
|
590
|
+
streaming: config?.streaming ?? DEFAULT_LLM_MONITORING_CONFIG.streaming
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
//#endregion
|
|
595
|
+
//#region src/llm/provider-detector.ts
|
|
596
|
+
function hostMatches(host, suffixes) {
|
|
597
|
+
return suffixes.some((suffix) => host === suffix || host.endsWith(`.${suffix}`));
|
|
598
|
+
}
|
|
599
|
+
function includesAny(pathname, values) {
|
|
600
|
+
return values.some((value) => pathname.includes(value));
|
|
601
|
+
}
|
|
602
|
+
function detectLlmProvider(url$1, llmConfig) {
|
|
603
|
+
if (!url$1) return;
|
|
604
|
+
if (!normalizeLlmMonitoringConfig(llmConfig).enabled) return;
|
|
605
|
+
try {
|
|
606
|
+
const parsed = new URL(url$1);
|
|
607
|
+
const host = parsed.hostname.toLowerCase();
|
|
608
|
+
const path = parsed.pathname.toLowerCase();
|
|
609
|
+
const maybeProvider = [];
|
|
610
|
+
if (hostMatches(host, ["x.ai", "api.x.ai"])) maybeProvider.push({
|
|
611
|
+
provider: "xai",
|
|
612
|
+
providerName: "xai"
|
|
613
|
+
});
|
|
614
|
+
if (hostMatches(host, ["openai.com", "api.openai.com"]) || includesAny(path, [
|
|
615
|
+
"/v1/chat/completions",
|
|
616
|
+
"/v1/responses",
|
|
617
|
+
"/v1/completions"
|
|
618
|
+
]) && !host.includes("x.ai")) maybeProvider.push({
|
|
619
|
+
provider: "openai",
|
|
620
|
+
providerName: "openai"
|
|
621
|
+
});
|
|
622
|
+
if (hostMatches(host, ["anthropic.com", "api.anthropic.com"]) || includesAny(path, ["/v1/messages", "/v1/complete"]) && host.includes("anthropic")) maybeProvider.push({
|
|
623
|
+
provider: "anthropic",
|
|
624
|
+
providerName: "anthropic"
|
|
625
|
+
});
|
|
626
|
+
if (hostMatches(host, ["googleapis.com", "generativelanguage.googleapis.com"]) || includesAny(path, [
|
|
627
|
+
":generatecontent",
|
|
628
|
+
":streamgeneratecontent",
|
|
629
|
+
"/models/"
|
|
630
|
+
])) {
|
|
631
|
+
if (host.includes("google") || path.includes("generatecontent")) maybeProvider.push({
|
|
632
|
+
provider: "gemini",
|
|
633
|
+
providerName: "gemini"
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
return maybeProvider[0];
|
|
637
|
+
} catch {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function deriveOperationName(url$1) {
|
|
642
|
+
if (!url$1) return;
|
|
643
|
+
try {
|
|
644
|
+
const pathname = new URL(url$1).pathname.toLowerCase();
|
|
645
|
+
if (pathname.includes("/embeddings")) return "embeddings";
|
|
646
|
+
if (pathname.includes("/responses")) return "responses";
|
|
647
|
+
if (pathname.includes("/chat/completions") || pathname.includes("/messages")) return "chat.completions";
|
|
648
|
+
if (pathname.includes("/completions") || pathname.includes("/complete")) return "completions";
|
|
649
|
+
if (pathname.includes("generatecontent")) return "chat.completions";
|
|
650
|
+
return;
|
|
651
|
+
} catch {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
//#endregion
|
|
657
|
+
//#region src/llm/request-parser.ts
|
|
658
|
+
function tryParseJson(raw) {
|
|
659
|
+
if (!raw || raw.length === 0) return;
|
|
660
|
+
try {
|
|
661
|
+
const parsed = JSON.parse(raw);
|
|
662
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
663
|
+
return;
|
|
664
|
+
} catch {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function getStringField(obj, key) {
|
|
669
|
+
if (!obj) return;
|
|
670
|
+
const value = obj[key];
|
|
671
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
672
|
+
}
|
|
673
|
+
function parseLlmRequestData(provider, requestBody, url$1) {
|
|
674
|
+
const parsed = tryParseJson(requestBody);
|
|
675
|
+
const data = {};
|
|
676
|
+
if (provider === "openai" || provider === "xai") data.model = getStringField(parsed, "model") || getStringField(parsed, "response_model") || void 0;
|
|
677
|
+
else if (provider === "anthropic") data.model = getStringField(parsed, "model") || void 0;
|
|
678
|
+
else if (provider === "gemini") data.model = getStringField(parsed, "model") || getModelFromGeminiPath(url$1);
|
|
679
|
+
return data;
|
|
680
|
+
}
|
|
681
|
+
function getModelFromGeminiPath(url$1) {
|
|
682
|
+
if (!url$1) return;
|
|
683
|
+
try {
|
|
684
|
+
const pathname = new URL(url$1).pathname;
|
|
685
|
+
const markerIndex = pathname.indexOf("/models/");
|
|
686
|
+
if (markerIndex < 0) return;
|
|
687
|
+
const modelPart = pathname.slice(markerIndex + 8);
|
|
688
|
+
const endIndex = modelPart.indexOf(":");
|
|
689
|
+
const candidate = endIndex >= 0 ? modelPart.slice(0, endIndex) : modelPart;
|
|
690
|
+
return candidate.length > 0 ? candidate : void 0;
|
|
691
|
+
} catch {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
//#endregion
|
|
697
|
+
//#region src/llm/response-parser.ts
|
|
698
|
+
function toNonNegativeNumber(value) {
|
|
699
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 0) return;
|
|
700
|
+
return value;
|
|
701
|
+
}
|
|
702
|
+
function parseOpenAiLikeUsage(response) {
|
|
703
|
+
const usage = typeof response.usage === "object" && response.usage !== null ? response.usage : void 0;
|
|
704
|
+
const promptDetails = usage && typeof usage.prompt_tokens_details === "object" && usage.prompt_tokens_details !== null ? usage.prompt_tokens_details : void 0;
|
|
705
|
+
return {
|
|
706
|
+
responseModel: typeof response.model === "string" ? response.model : void 0,
|
|
707
|
+
responseId: typeof response.id === "string" ? response.id : void 0,
|
|
708
|
+
inputTokens: usage ? toNonNegativeNumber(usage.prompt_tokens) : void 0,
|
|
709
|
+
outputTokens: usage ? toNonNegativeNumber(usage.completion_tokens) : void 0,
|
|
710
|
+
totalTokens: usage ? toNonNegativeNumber(usage.total_tokens) : void 0,
|
|
711
|
+
cacheReadInputTokens: promptDetails ? toNonNegativeNumber(promptDetails.cached_tokens) : void 0
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
function parseAnthropicUsage(response) {
|
|
715
|
+
const message = typeof response.message === "object" && response.message !== null ? response.message : void 0;
|
|
716
|
+
const usage = typeof response.usage === "object" && response.usage !== null ? response.usage : message && typeof message.usage === "object" && message.usage !== null ? message.usage : void 0;
|
|
717
|
+
return {
|
|
718
|
+
responseModel: typeof response.model === "string" ? response.model : typeof message?.model === "string" ? message.model : void 0,
|
|
719
|
+
responseId: typeof response.id === "string" ? response.id : typeof message?.id === "string" ? message.id : void 0,
|
|
720
|
+
inputTokens: usage ? toNonNegativeNumber(usage.input_tokens) : void 0,
|
|
721
|
+
outputTokens: usage ? toNonNegativeNumber(usage.output_tokens) : void 0,
|
|
722
|
+
totalTokens: usage && (toNonNegativeNumber(usage.input_tokens) !== void 0 || toNonNegativeNumber(usage.output_tokens) !== void 0) ? (toNonNegativeNumber(usage.input_tokens) ?? 0) + (toNonNegativeNumber(usage.output_tokens) ?? 0) : void 0,
|
|
723
|
+
cacheReadInputTokens: usage ? toNonNegativeNumber(usage.cache_read_input_tokens) : void 0,
|
|
724
|
+
cacheCreationInputTokens: usage ? toNonNegativeNumber(usage.cache_creation_input_tokens) : void 0
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function parseGeminiUsage(response) {
|
|
728
|
+
const usage = typeof response.usageMetadata === "object" && response.usageMetadata !== null ? response.usageMetadata : void 0;
|
|
729
|
+
let responseModel;
|
|
730
|
+
if (typeof response.modelVersion === "string") responseModel = response.modelVersion;
|
|
731
|
+
else if (Array.isArray(response.candidates) && response.candidates.length > 0) {
|
|
732
|
+
const first = response.candidates[0];
|
|
733
|
+
if (typeof first === "object" && first !== null && "model" in first) responseModel = typeof first.model === "string" ? first.model : void 0;
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
responseModel,
|
|
737
|
+
responseId: typeof response.responseId === "string" ? response.responseId : void 0,
|
|
738
|
+
inputTokens: usage ? toNonNegativeNumber(usage.promptTokenCount) : void 0,
|
|
739
|
+
outputTokens: usage ? toNonNegativeNumber(usage.candidatesTokenCount) : void 0,
|
|
740
|
+
totalTokens: usage ? toNonNegativeNumber(usage.totalTokenCount) : void 0,
|
|
741
|
+
cacheReadInputTokens: usage ? toNonNegativeNumber(usage.cachedContentTokenCount) : void 0
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function parseJsonObject(raw) {
|
|
745
|
+
if (!raw || raw.length === 0) return;
|
|
746
|
+
try {
|
|
747
|
+
const value = JSON.parse(raw);
|
|
748
|
+
if (typeof value === "object" && value !== null) return value;
|
|
749
|
+
return;
|
|
750
|
+
} catch {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function mergeUsage(target, next) {
|
|
755
|
+
return {
|
|
756
|
+
responseModel: next.responseModel ?? target.responseModel,
|
|
757
|
+
responseId: next.responseId ?? target.responseId,
|
|
758
|
+
inputTokens: next.inputTokens ?? target.inputTokens,
|
|
759
|
+
outputTokens: next.outputTokens ?? target.outputTokens,
|
|
760
|
+
totalTokens: next.totalTokens ?? target.totalTokens,
|
|
761
|
+
cacheReadInputTokens: next.cacheReadInputTokens ?? target.cacheReadInputTokens,
|
|
762
|
+
cacheCreationInputTokens: next.cacheCreationInputTokens ?? target.cacheCreationInputTokens
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function parseByProvider(provider, object) {
|
|
766
|
+
if (provider === "openai" || provider === "xai") return parseOpenAiLikeUsage(object);
|
|
767
|
+
if (provider === "anthropic") return parseAnthropicUsage(object);
|
|
768
|
+
return parseGeminiUsage(object);
|
|
769
|
+
}
|
|
770
|
+
function parseSsePayload(provider, raw) {
|
|
771
|
+
if (!raw || raw.length === 0) return {};
|
|
772
|
+
let acc = {};
|
|
773
|
+
const lines = raw.split(/\r?\n/);
|
|
774
|
+
for (const line of lines) {
|
|
775
|
+
const trimmed = line.trim();
|
|
776
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
777
|
+
const payload = trimmed.slice(5).trim();
|
|
778
|
+
if (payload.length === 0 || payload === "[DONE]") continue;
|
|
779
|
+
const parsed = parseJsonObject(payload);
|
|
780
|
+
if (!parsed) continue;
|
|
781
|
+
acc = mergeUsage(acc, parseByProvider(provider, parsed));
|
|
782
|
+
}
|
|
783
|
+
return acc;
|
|
784
|
+
}
|
|
785
|
+
function parseLlmResponseData(provider, responseBody, streaming = true) {
|
|
786
|
+
if (!responseBody || responseBody.length === 0) return {};
|
|
787
|
+
const parsedJson = parseJsonObject(responseBody);
|
|
788
|
+
if (parsedJson) return parseByProvider(provider, parsedJson);
|
|
789
|
+
if (!streaming) return {};
|
|
790
|
+
return parseSsePayload(provider, responseBody);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
//#endregion
|
|
794
|
+
//#region src/llm/enricher.ts
|
|
795
|
+
const logger = (0, _pingops_core.createLogger)("[PingOps LLM Enricher]");
|
|
796
|
+
function getEffectiveConfig() {
|
|
797
|
+
return normalizeLlmMonitoringConfig(getGlobalConfig()?.llmMonitoring);
|
|
798
|
+
}
|
|
799
|
+
function createLlmEnrichmentState(span, url$1, requestMethod, requestHeaders) {
|
|
800
|
+
const config = getEffectiveConfig();
|
|
801
|
+
if (!config.enabled) return;
|
|
802
|
+
const detection = detectLlmProvider(url$1, config);
|
|
803
|
+
if (!detection) return;
|
|
804
|
+
return {
|
|
805
|
+
span,
|
|
806
|
+
url: url$1,
|
|
807
|
+
requestMethod,
|
|
808
|
+
requestHeaders,
|
|
809
|
+
detection,
|
|
810
|
+
requestData: void 0,
|
|
811
|
+
responseData: void 0,
|
|
812
|
+
responseHeaders: void 0,
|
|
813
|
+
requestParseBytes: 0,
|
|
814
|
+
responseParseBytes: 0,
|
|
815
|
+
requestBodyBuffer: [],
|
|
816
|
+
responseBodyBuffer: [],
|
|
817
|
+
requestParseStopped: false,
|
|
818
|
+
responseParseStopped: false
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function updateLlmRequestHeaders(state, headers) {
|
|
822
|
+
state.requestHeaders = headers;
|
|
823
|
+
}
|
|
824
|
+
function updateLlmResponseHeaders(state, headers) {
|
|
825
|
+
state.responseHeaders = headers;
|
|
826
|
+
}
|
|
827
|
+
function appendLlmRequestChunk(state, chunk) {
|
|
828
|
+
state.requestBodyBuffer.push(chunk);
|
|
829
|
+
state.requestParseBytes += chunk.length;
|
|
830
|
+
}
|
|
831
|
+
function appendLlmResponseChunk(state, chunk) {
|
|
832
|
+
state.responseBodyBuffer.push(chunk);
|
|
833
|
+
state.responseParseBytes += chunk.length;
|
|
834
|
+
}
|
|
835
|
+
function computeTotalTokens(responseData) {
|
|
836
|
+
if (responseData.totalTokens !== void 0) return responseData.totalTokens;
|
|
837
|
+
if (responseData.inputTokens !== void 0 || responseData.outputTokens !== void 0) return (responseData.inputTokens ?? 0) + (responseData.outputTokens ?? 0);
|
|
838
|
+
}
|
|
839
|
+
function normalizeHeaderValue(value) {
|
|
840
|
+
if (typeof value === "string") return value;
|
|
841
|
+
if (Array.isArray(value) && value.length > 0) return value.join(",");
|
|
842
|
+
}
|
|
843
|
+
function decodeResponseBody(body, headers) {
|
|
844
|
+
const encodingValue = normalizeHeaderValue(headers?.["content-encoding"]);
|
|
845
|
+
if (!encodingValue || encodingValue.trim().length === 0) return body.toString("utf8");
|
|
846
|
+
const encodings = encodingValue.toLowerCase().split(",").map((v) => v.trim()).filter(Boolean);
|
|
847
|
+
if (encodings.length === 0 || encodings.includes("identity")) return body.toString("utf8");
|
|
848
|
+
let decoded = body;
|
|
849
|
+
for (let i = encodings.length - 1; i >= 0; i -= 1) {
|
|
850
|
+
const encoding = encodings[i];
|
|
851
|
+
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
852
|
+
decoded = (0, zlib.gunzipSync)(decoded);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (encoding === "deflate") {
|
|
856
|
+
decoded = (0, zlib.inflateSync)(decoded);
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (encoding === "br") {
|
|
860
|
+
decoded = (0, zlib.brotliDecompressSync)(decoded);
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
return body.toString("utf8");
|
|
864
|
+
}
|
|
865
|
+
return decoded.toString("utf8");
|
|
866
|
+
}
|
|
867
|
+
function finalizeLlmEnrichment(state) {
|
|
868
|
+
const config = getEffectiveConfig();
|
|
869
|
+
if (!config.enabled || !state.detection) return;
|
|
870
|
+
try {
|
|
871
|
+
const requestBody = state.requestBodyBuffer.length > 0 ? Buffer.concat(state.requestBodyBuffer).toString("utf8") : void 0;
|
|
872
|
+
if (state.requestData === void 0) state.requestData = parseLlmRequestData(state.detection.provider, requestBody, state.url);
|
|
873
|
+
const operationName = deriveOperationName(state.url);
|
|
874
|
+
const responseBody = state.responseBodyBuffer.length > 0 ? decodeResponseBody(Buffer.concat(state.responseBodyBuffer), state.responseHeaders) : void 0;
|
|
875
|
+
const parsedResponse = responseBody ? parseLlmResponseData(state.detection.provider, responseBody, config.streaming) : {};
|
|
876
|
+
state.responseData = {
|
|
877
|
+
...state.responseData,
|
|
878
|
+
...parsedResponse
|
|
879
|
+
};
|
|
880
|
+
state.span.setAttribute(GEN_AI_ATTRS.SYSTEM, state.detection.provider);
|
|
881
|
+
state.span.setAttribute(GEN_AI_ATTRS.PROVIDER_NAME, state.detection.providerName);
|
|
882
|
+
if (operationName) state.span.setAttribute(GEN_AI_ATTRS.OPERATION_NAME, operationName);
|
|
883
|
+
if (state.requestData?.model) state.span.setAttribute(GEN_AI_ATTRS.REQUEST_MODEL, state.requestData.model);
|
|
884
|
+
if (state.responseData?.responseModel) state.span.setAttribute(GEN_AI_ATTRS.RESPONSE_MODEL, state.responseData.responseModel);
|
|
885
|
+
if (state.responseData?.responseId) state.span.setAttribute(GEN_AI_ATTRS.RESPONSE_ID, state.responseData.responseId);
|
|
886
|
+
if (state.responseData?.inputTokens !== void 0) state.span.setAttribute(GEN_AI_ATTRS.USAGE_INPUT_TOKENS, state.responseData.inputTokens);
|
|
887
|
+
if (state.responseData?.outputTokens !== void 0) state.span.setAttribute(GEN_AI_ATTRS.USAGE_OUTPUT_TOKENS, state.responseData.outputTokens);
|
|
888
|
+
const totalTokens = state.responseData ? computeTotalTokens(state.responseData) : void 0;
|
|
889
|
+
if (totalTokens !== void 0) state.span.setAttribute(PINGOPS_GEN_AI_ATTRS.TOTAL_TOKENS, totalTokens);
|
|
890
|
+
if (state.responseData?.cacheReadInputTokens !== void 0) state.span.setAttribute(PINGOPS_GEN_AI_ATTRS.CACHE_READ_INPUT_TOKENS, state.responseData.cacheReadInputTokens);
|
|
891
|
+
if (state.responseData?.cacheCreationInputTokens !== void 0) state.span.setAttribute(PINGOPS_GEN_AI_ATTRS.CACHE_CREATION_INPUT_TOKENS, state.responseData.cacheCreationInputTokens);
|
|
892
|
+
const cacheTokens = (state.responseData?.cacheReadInputTokens ?? 0) + (state.responseData?.cacheCreationInputTokens ?? 0);
|
|
893
|
+
if (cacheTokens > 0) state.span.setAttribute(PINGOPS_GEN_AI_ATTRS.CACHE_TOKENS, cacheTokens);
|
|
894
|
+
} catch (error) {
|
|
895
|
+
logger.debug("Failed to enrich LLM attributes", {
|
|
896
|
+
error: error instanceof Error ? error.message : String(error),
|
|
897
|
+
url: state.url,
|
|
898
|
+
provider: state.detection.provider
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
434
903
|
//#endregion
|
|
435
904
|
//#region src/instrumentations/http/pingops-http.ts
|
|
436
905
|
/**
|
|
437
906
|
* Pingops HTTP instrumentation that extends HttpInstrumentation
|
|
438
907
|
* with request/response body capture
|
|
439
908
|
*/
|
|
440
|
-
const DEFAULT_MAX_REQUEST_BODY_SIZE$1 = 4 * 1024;
|
|
441
|
-
const DEFAULT_MAX_RESPONSE_BODY_SIZE$1 = 4 * 1024;
|
|
442
909
|
const LEGACY_ATTR_HTTP_URL = "http.url";
|
|
443
910
|
const PingopsSemanticAttributes = {
|
|
444
|
-
HTTP_REQUEST_BODY
|
|
445
|
-
HTTP_RESPONSE_BODY
|
|
911
|
+
HTTP_REQUEST_BODY,
|
|
912
|
+
HTTP_RESPONSE_BODY,
|
|
913
|
+
HTTP_REQUEST_BODY_SIZE,
|
|
914
|
+
HTTP_RESPONSE_BODY_SIZE
|
|
446
915
|
};
|
|
447
916
|
/**
|
|
448
917
|
* Manually flattens a nested object into dot-notation keys
|
|
@@ -455,7 +924,7 @@ function isPrimitiveArray(value) {
|
|
|
455
924
|
}
|
|
456
925
|
function flatten(obj, prefix = "") {
|
|
457
926
|
const result = {};
|
|
458
|
-
for (const key in obj) if (Object.
|
|
927
|
+
for (const key in obj) if (Object.hasOwn(obj, key)) {
|
|
459
928
|
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
460
929
|
const value = obj[key];
|
|
461
930
|
if (isPlainObject(value)) Object.assign(result, flatten(value, newKey));
|
|
@@ -474,61 +943,12 @@ function setAttributeValue(span, attrName, attrValue) {
|
|
|
474
943
|
} else if (isPlainObject(attrValue)) span.setAttributes(flatten({ [attrName]: attrValue }));
|
|
475
944
|
}
|
|
476
945
|
/**
|
|
477
|
-
*
|
|
478
|
-
*/
|
|
479
|
-
function extractDomainFromUrl$1(url$1) {
|
|
480
|
-
try {
|
|
481
|
-
return new URL(url$1).hostname;
|
|
482
|
-
} catch {
|
|
483
|
-
const match = url$1.match(/^(?:https?:\/\/)?([^/]+)/);
|
|
484
|
-
return match ? match[1] : "";
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* Gets domain rule configuration for a given URL
|
|
489
|
-
*/
|
|
490
|
-
function getDomainRule$1(url$1, domainAllowList) {
|
|
491
|
-
if (!domainAllowList) return;
|
|
492
|
-
const domain = extractDomainFromUrl$1(url$1);
|
|
493
|
-
for (const rule of domainAllowList) if (domain === rule.domain || domain.endsWith(`.${rule.domain}`) || domain === rule.domain.slice(1)) return rule;
|
|
494
|
-
}
|
|
495
|
-
/**
|
|
496
|
-
* Determines if request body should be captured based on priority:
|
|
497
|
-
* context > domain rule > global config > default (false)
|
|
946
|
+
* Captures request body from a chunk buffer.
|
|
498
947
|
*/
|
|
499
|
-
function
|
|
500
|
-
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
const domainRule = getDomainRule$1(url$1, getGlobalConfig()?.domainAllowList);
|
|
504
|
-
if (domainRule?.captureRequestBody !== void 0) return domainRule.captureRequestBody;
|
|
505
|
-
}
|
|
506
|
-
const globalConfig$1 = getGlobalConfig();
|
|
507
|
-
if (globalConfig$1?.captureRequestBody !== void 0) return globalConfig$1.captureRequestBody;
|
|
508
|
-
return false;
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* Determines if response body should be captured based on priority:
|
|
512
|
-
* context > domain rule > global config > default (false)
|
|
513
|
-
*/
|
|
514
|
-
function shouldCaptureResponseBody$1(url$1) {
|
|
515
|
-
const contextValue = _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_CAPTURE_RESPONSE_BODY);
|
|
516
|
-
if (contextValue !== void 0) return contextValue;
|
|
517
|
-
if (url$1) {
|
|
518
|
-
const domainRule = getDomainRule$1(url$1, getGlobalConfig()?.domainAllowList);
|
|
519
|
-
if (domainRule?.captureResponseBody !== void 0) return domainRule.captureResponseBody;
|
|
520
|
-
}
|
|
521
|
-
const globalConfig$1 = getGlobalConfig();
|
|
522
|
-
if (globalConfig$1?.captureResponseBody !== void 0) return globalConfig$1.captureResponseBody;
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
/**
|
|
526
|
-
* Captures request body from string or Buffer data
|
|
527
|
-
*/
|
|
528
|
-
function captureRequestBody(span, data, maxSize, semanticAttr, url$1) {
|
|
529
|
-
if (!shouldCaptureRequestBody$1(url$1)) return;
|
|
530
|
-
if (data.length && data.length <= maxSize) try {
|
|
531
|
-
const requestBody = typeof data === "string" ? data : data.toString("utf-8");
|
|
948
|
+
function captureRequestBody(span, data, semanticAttr, url$1) {
|
|
949
|
+
if (!shouldCaptureRequestBody(url$1)) return;
|
|
950
|
+
if (data.length) try {
|
|
951
|
+
const requestBody = data.toString("utf-8");
|
|
532
952
|
if (requestBody) setAttributeValue(span, semanticAttr, requestBody);
|
|
533
953
|
} catch (e) {
|
|
534
954
|
console.error("Error occurred while capturing request body:", e);
|
|
@@ -537,25 +957,13 @@ function captureRequestBody(span, data, maxSize, semanticAttr, url$1) {
|
|
|
537
957
|
/**
|
|
538
958
|
* Captures response body from chunks
|
|
539
959
|
*/
|
|
540
|
-
function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url$1
|
|
541
|
-
if (!shouldCaptureResponseBody
|
|
542
|
-
if (chunks === null) {
|
|
543
|
-
const contentEncoding = responseHeaders?.["content-encoding"];
|
|
544
|
-
const contentType = responseHeaders?.["content-type"];
|
|
545
|
-
const toHeaderString = (value) => typeof value === "string" ? value : Array.isArray(value) ? value.join(", ") : "unknown";
|
|
546
|
-
setAttributeValue(span, semanticAttr, `[truncated response body; exceeded maxResponseBodySize=${maxSize ?? DEFAULT_MAX_RESPONSE_BODY_SIZE$1}; content-type=${toHeaderString(contentType)}; content-encoding=${toHeaderString(contentEncoding)}]`);
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
960
|
+
function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url$1) {
|
|
961
|
+
if (!shouldCaptureResponseBody(url$1)) return;
|
|
549
962
|
if (chunks.length) try {
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
setAttributeValue(span,
|
|
554
|
-
const encStr = typeof contentEncoding === "string" ? contentEncoding : Array.isArray(contentEncoding) ? contentEncoding.map(String).join(", ") : void 0;
|
|
555
|
-
if (encStr) setAttributeValue(span, _pingops_core.HTTP_RESPONSE_CONTENT_ENCODING, encStr);
|
|
556
|
-
} else {
|
|
557
|
-
const bodyStr = (0, _pingops_core.bufferToBodyString)(concatedChunks);
|
|
558
|
-
if (bodyStr != null) setAttributeValue(span, semanticAttr, bodyStr);
|
|
963
|
+
const encoded = encodeBodyBufferForSpan(Buffer.concat(chunks), responseHeaders);
|
|
964
|
+
if (encoded) {
|
|
965
|
+
setAttributeValue(span, semanticAttr, encoded.content);
|
|
966
|
+
if (encoded.contentEncoding) setAttributeValue(span, _pingops_core.HTTP_RESPONSE_CONTENT_ENCODING, encoded.contentEncoding);
|
|
559
967
|
}
|
|
560
968
|
} catch (e) {
|
|
561
969
|
console.error("Error occurred while capturing response body:", e);
|
|
@@ -632,6 +1040,7 @@ function extractClientRequestPath(request) {
|
|
|
632
1040
|
}
|
|
633
1041
|
const PingopsHttpSemanticAttributes = PingopsSemanticAttributes;
|
|
634
1042
|
var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_http.HttpInstrumentation {
|
|
1043
|
+
_llmStateByRequest = /* @__PURE__ */ new WeakMap();
|
|
635
1044
|
constructor(config) {
|
|
636
1045
|
super(config);
|
|
637
1046
|
this._config = this._createConfig(config);
|
|
@@ -653,19 +1062,26 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
653
1062
|
_createConfig(config) {
|
|
654
1063
|
return {
|
|
655
1064
|
...config,
|
|
656
|
-
requestHook: this._createRequestHook(config?.requestHook
|
|
657
|
-
responseHook: this._createResponseHook(config?.responseHook
|
|
1065
|
+
requestHook: this._createRequestHook(config?.requestHook),
|
|
1066
|
+
responseHook: this._createResponseHook(config?.responseHook)
|
|
658
1067
|
};
|
|
659
1068
|
}
|
|
660
|
-
_createRequestHook(originalRequestHook
|
|
1069
|
+
_createRequestHook(originalRequestHook) {
|
|
661
1070
|
return (span, request) => {
|
|
662
1071
|
const headers = extractRequestHeaders(request);
|
|
663
1072
|
if (headers) captureRequestHeaders(span, headers);
|
|
664
1073
|
if (request instanceof http.ClientRequest) {
|
|
665
|
-
|
|
1074
|
+
let requestBodySize = 0;
|
|
1075
|
+
const requestContentLength = parseContentLength(headers?.["content-length"]);
|
|
1076
|
+
span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestContentLength !== void 0 ? Math.max(requestBodySize, requestContentLength) : requestBodySize);
|
|
666
1077
|
const hostHeader = request.getHeader("host");
|
|
667
1078
|
const host = typeof hostHeader === "string" ? hostHeader : Array.isArray(hostHeader) ? hostHeader.join(",") : typeof hostHeader === "number" ? String(hostHeader) : void 0;
|
|
668
1079
|
const url$1 = request.path && host ? `${request.protocol || "http:"}//${host}${request.path}` : void 0;
|
|
1080
|
+
const llmState = createLlmEnrichmentState(span, url$1, request.method, headers ?? void 0);
|
|
1081
|
+
if (llmState) {
|
|
1082
|
+
updateLlmRequestHeaders(llmState, headers ?? {});
|
|
1083
|
+
this._llmStateByRequest.set(request, llmState);
|
|
1084
|
+
}
|
|
669
1085
|
if (typeof request.path === "string" && request.path.length > 0) {
|
|
670
1086
|
const { path, query } = parseRequestPathAndQuery(request.path);
|
|
671
1087
|
span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_PATH, path);
|
|
@@ -675,49 +1091,68 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
675
1091
|
const originalWrite = request.write.bind(request);
|
|
676
1092
|
const originalEnd = request.end.bind(request);
|
|
677
1093
|
request.write = ((data, ...rest) => {
|
|
678
|
-
|
|
1094
|
+
const chunkBuffer = toBufferChunk(data);
|
|
1095
|
+
if (chunkBuffer) {
|
|
1096
|
+
requestBodySize += chunkBuffer.length;
|
|
1097
|
+
span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestContentLength !== void 0 ? Math.max(requestBodySize, requestContentLength) : requestBodySize);
|
|
1098
|
+
captureRequestBody(span, chunkBuffer, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url$1);
|
|
1099
|
+
if (llmState) appendLlmRequestChunk(llmState, chunkBuffer);
|
|
1100
|
+
}
|
|
679
1101
|
return originalWrite(data, ...rest);
|
|
680
1102
|
});
|
|
681
1103
|
request.end = ((data, ...rest) => {
|
|
682
|
-
|
|
1104
|
+
const chunkBuffer = toBufferChunk(data);
|
|
1105
|
+
if (chunkBuffer) {
|
|
1106
|
+
requestBodySize += chunkBuffer.length;
|
|
1107
|
+
span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestContentLength !== void 0 ? Math.max(requestBodySize, requestContentLength) : requestBodySize);
|
|
1108
|
+
captureRequestBody(span, chunkBuffer, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url$1);
|
|
1109
|
+
if (llmState) appendLlmRequestChunk(llmState, chunkBuffer);
|
|
1110
|
+
}
|
|
683
1111
|
return originalEnd(data, ...rest);
|
|
684
1112
|
});
|
|
685
1113
|
}
|
|
686
1114
|
if (originalRequestHook) originalRequestHook(span, request);
|
|
687
1115
|
};
|
|
688
1116
|
}
|
|
689
|
-
_createResponseHook(originalResponseHook
|
|
1117
|
+
_createResponseHook(originalResponseHook) {
|
|
690
1118
|
return (span, response) => {
|
|
691
1119
|
const headers = extractResponseHeaders(response);
|
|
692
1120
|
if (headers) captureResponseHeaders(span, headers);
|
|
693
1121
|
if (response instanceof http.IncomingMessage) {
|
|
1122
|
+
const requestForState = response.req instanceof http.ClientRequest ? response.req : void 0;
|
|
1123
|
+
const llmState = requestForState ? this._llmStateByRequest.get(requestForState) : void 0;
|
|
694
1124
|
const requestPath = response.req instanceof http.ClientRequest ? extractClientRequestPath(response.req) : void 0;
|
|
695
1125
|
if (requestPath) {
|
|
696
1126
|
const { path, query } = parseRequestPathAndQuery(requestPath);
|
|
697
1127
|
span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_PATH, path);
|
|
698
1128
|
if (query) span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_QUERY, query);
|
|
699
1129
|
}
|
|
700
|
-
const maxResponseBodySize = config?.maxResponseBodySize || DEFAULT_MAX_RESPONSE_BODY_SIZE$1;
|
|
701
1130
|
const url$1 = response.url || void 0;
|
|
702
|
-
|
|
1131
|
+
const chunks = [];
|
|
703
1132
|
let totalSize = 0;
|
|
704
|
-
|
|
1133
|
+
span.setAttribute(PingopsSemanticAttributes.HTTP_RESPONSE_BODY_SIZE, 0);
|
|
1134
|
+
const shouldCapture = shouldCaptureResponseBody(url$1);
|
|
1135
|
+
if (llmState && headers) updateLlmResponseHeaders(llmState, headers);
|
|
705
1136
|
response.prependListener("data", (chunk) => {
|
|
706
|
-
if (!chunk
|
|
707
|
-
|
|
708
|
-
if (typeof chunk === "string") chunkBuffer = Buffer.from(chunk);
|
|
709
|
-
else if (Buffer.isBuffer(chunk)) chunkBuffer = chunk;
|
|
710
|
-
else if (chunk instanceof Uint8Array) chunkBuffer = Buffer.from(chunk);
|
|
1137
|
+
if (!chunk) return;
|
|
1138
|
+
const chunkBuffer = toBufferChunk(chunk);
|
|
711
1139
|
if (!chunkBuffer) return;
|
|
712
1140
|
totalSize += chunkBuffer.length;
|
|
713
|
-
|
|
714
|
-
|
|
1141
|
+
span.setAttribute(PingopsSemanticAttributes.HTTP_RESPONSE_BODY_SIZE, totalSize);
|
|
1142
|
+
if (llmState) appendLlmResponseChunk(llmState, chunkBuffer);
|
|
1143
|
+
if (!shouldCapture) return;
|
|
1144
|
+
chunks.push(chunkBuffer);
|
|
715
1145
|
});
|
|
716
1146
|
let finalized = false;
|
|
717
1147
|
const finalizeCapture = () => {
|
|
718
1148
|
if (finalized) return;
|
|
719
1149
|
finalized = true;
|
|
720
|
-
|
|
1150
|
+
const contentLength = parseContentLength(headers?.["content-length"]);
|
|
1151
|
+
const responseBodySize = contentLength !== void 0 ? Math.max(totalSize, contentLength) : totalSize;
|
|
1152
|
+
span.setAttribute(PingopsSemanticAttributes.HTTP_RESPONSE_BODY_SIZE, responseBodySize);
|
|
1153
|
+
captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, headers, url$1);
|
|
1154
|
+
if (llmState) finalizeLlmEnrichment(llmState);
|
|
1155
|
+
if (requestForState) this._llmStateByRequest.delete(requestForState);
|
|
721
1156
|
};
|
|
722
1157
|
response.prependOnceListener("end", finalizeCapture);
|
|
723
1158
|
response.prependOnceListener("close", finalizeCapture);
|
|
@@ -749,7 +1184,6 @@ function toRequestUrl$1(request) {
|
|
|
749
1184
|
* @returns PingopsHttpInstrumentation instance
|
|
750
1185
|
*/
|
|
751
1186
|
function createHttpInstrumentation(config) {
|
|
752
|
-
const globalConfig$1 = getGlobalConfig();
|
|
753
1187
|
const userIgnoreOutgoingRequestHook = config?.ignoreOutgoingRequestHook;
|
|
754
1188
|
return new PingopsHttpInstrumentation({
|
|
755
1189
|
...config,
|
|
@@ -757,67 +1191,12 @@ function createHttpInstrumentation(config) {
|
|
|
757
1191
|
ignoreOutgoingRequestHook: (request) => {
|
|
758
1192
|
if (shouldIgnoreOutboundInstrumentation(toRequestUrl$1(request))) return true;
|
|
759
1193
|
return userIgnoreOutgoingRequestHook?.(request) ?? false;
|
|
760
|
-
}
|
|
761
|
-
maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
|
|
762
|
-
maxResponseBodySize: globalConfig$1?.maxResponseBodySize
|
|
1194
|
+
}
|
|
763
1195
|
});
|
|
764
1196
|
}
|
|
765
1197
|
|
|
766
1198
|
//#endregion
|
|
767
1199
|
//#region src/instrumentations/undici/pingops-undici.ts
|
|
768
|
-
const DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024;
|
|
769
|
-
const DEFAULT_MAX_RESPONSE_BODY_SIZE = 4 * 1024;
|
|
770
|
-
const HTTP_REQUEST_BODY = "http.request.body";
|
|
771
|
-
const HTTP_RESPONSE_BODY = "http.response.body";
|
|
772
|
-
/**
|
|
773
|
-
* Extracts domain from URL
|
|
774
|
-
*/
|
|
775
|
-
function extractDomainFromUrl(url$1) {
|
|
776
|
-
try {
|
|
777
|
-
return new url.URL(url$1).hostname;
|
|
778
|
-
} catch {
|
|
779
|
-
const match = url$1.match(/^(?:https?:\/\/)?([^/]+)/);
|
|
780
|
-
return match ? match[1] : "";
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Gets domain rule configuration for a given URL
|
|
785
|
-
*/
|
|
786
|
-
function getDomainRule(url$1, domainAllowList) {
|
|
787
|
-
if (!domainAllowList) return;
|
|
788
|
-
const domain = extractDomainFromUrl(url$1);
|
|
789
|
-
for (const rule of domainAllowList) if (domain === rule.domain || domain.endsWith(`.${rule.domain}`) || domain === rule.domain.slice(1)) return rule;
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Determines if request body should be captured based on priority:
|
|
793
|
-
* context > domain rule > global config > default (false)
|
|
794
|
-
*/
|
|
795
|
-
function shouldCaptureRequestBody(url$1) {
|
|
796
|
-
const contextValue = _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_CAPTURE_REQUEST_BODY);
|
|
797
|
-
if (contextValue !== void 0) return contextValue;
|
|
798
|
-
if (url$1) {
|
|
799
|
-
const domainRule = getDomainRule(url$1, getGlobalConfig()?.domainAllowList);
|
|
800
|
-
if (domainRule?.captureRequestBody !== void 0) return domainRule.captureRequestBody;
|
|
801
|
-
}
|
|
802
|
-
const globalConfig$1 = getGlobalConfig();
|
|
803
|
-
if (globalConfig$1?.captureRequestBody !== void 0) return globalConfig$1.captureRequestBody;
|
|
804
|
-
return false;
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Determines if response body should be captured based on priority:
|
|
808
|
-
* context > domain rule > global config > default (false)
|
|
809
|
-
*/
|
|
810
|
-
function shouldCaptureResponseBody(url$1) {
|
|
811
|
-
const contextValue = _opentelemetry_api.context.active().getValue(_pingops_core.PINGOPS_CAPTURE_RESPONSE_BODY);
|
|
812
|
-
if (contextValue !== void 0) return contextValue;
|
|
813
|
-
if (url$1) {
|
|
814
|
-
const domainRule = getDomainRule(url$1, getGlobalConfig()?.domainAllowList);
|
|
815
|
-
if (domainRule?.captureResponseBody !== void 0) return domainRule.captureResponseBody;
|
|
816
|
-
}
|
|
817
|
-
const globalConfig$1 = getGlobalConfig();
|
|
818
|
-
if (globalConfig$1?.captureResponseBody !== void 0) return globalConfig$1.captureResponseBody;
|
|
819
|
-
return false;
|
|
820
|
-
}
|
|
821
1200
|
var UndiciInstrumentation = class extends _opentelemetry_instrumentation.InstrumentationBase {
|
|
822
1201
|
_recordFromReq = /* @__PURE__ */ new WeakMap();
|
|
823
1202
|
constructor(config = {}) {
|
|
@@ -925,7 +1304,9 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
925
1304
|
[_opentelemetry_semantic_conventions.ATTR_URL_FULL]: requestUrl.toString(),
|
|
926
1305
|
[_opentelemetry_semantic_conventions.ATTR_URL_PATH]: requestUrl.pathname,
|
|
927
1306
|
[_opentelemetry_semantic_conventions.ATTR_URL_QUERY]: requestUrl.search,
|
|
928
|
-
[_opentelemetry_semantic_conventions.ATTR_URL_SCHEME]: urlScheme
|
|
1307
|
+
[_opentelemetry_semantic_conventions.ATTR_URL_SCHEME]: urlScheme,
|
|
1308
|
+
[HTTP_REQUEST_BODY_SIZE]: 0,
|
|
1309
|
+
[HTTP_RESPONSE_BODY_SIZE]: 0
|
|
929
1310
|
};
|
|
930
1311
|
const schemePorts = {
|
|
931
1312
|
https: "443",
|
|
@@ -935,7 +1316,10 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
935
1316
|
const serverPort = requestUrl.port || schemePorts[urlScheme];
|
|
936
1317
|
attributes[_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS] = serverAddress;
|
|
937
1318
|
if (serverPort && !isNaN(Number(serverPort))) attributes[_opentelemetry_semantic_conventions.ATTR_SERVER_PORT] = Number(serverPort);
|
|
938
|
-
const
|
|
1319
|
+
const headersMap = this.parseRequestHeaders(request);
|
|
1320
|
+
const requestHeadersObject = {};
|
|
1321
|
+
for (const [key, value] of headersMap.entries()) requestHeadersObject[key] = value;
|
|
1322
|
+
const userAgentValues = headersMap.get("user-agent");
|
|
939
1323
|
if (userAgentValues) attributes[_opentelemetry_semantic_conventions.ATTR_USER_AGENT_ORIGINAL] = Array.isArray(userAgentValues) ? userAgentValues[userAgentValues.length - 1] : userAgentValues;
|
|
940
1324
|
const hookAttributes = (0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => config.startSpanHook?.(request), (e) => e && this._diag.error("caught startSpanHook error: ", e), true);
|
|
941
1325
|
if (hookAttributes) Object.entries(hookAttributes).forEach(([key, val]) => {
|
|
@@ -968,13 +1352,16 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
968
1352
|
responseBodyChunks: [],
|
|
969
1353
|
requestBodySize: 0,
|
|
970
1354
|
responseBodySize: 0,
|
|
971
|
-
url: requestUrl.toString()
|
|
1355
|
+
url: requestUrl.toString(),
|
|
1356
|
+
llmState: createLlmEnrichmentState(span, requestUrl.toString(), request.method, requestHeadersObject)
|
|
972
1357
|
});
|
|
1358
|
+
const createdRecord = this._recordFromReq.get(request);
|
|
1359
|
+
if (createdRecord?.llmState) updateLlmRequestHeaders(createdRecord.llmState, requestHeadersObject);
|
|
973
1360
|
}
|
|
974
1361
|
onRequestHeaders({ request, socket }) {
|
|
975
1362
|
const record = this._recordFromReq.get(request);
|
|
976
1363
|
if (!record) return;
|
|
977
|
-
const { span } = record;
|
|
1364
|
+
const { span, attributes } = record;
|
|
978
1365
|
const spanAttributes = {};
|
|
979
1366
|
const remoteAddress = typeof socket.remoteAddress === "string" ? socket.remoteAddress : void 0;
|
|
980
1367
|
const remotePort = typeof socket.remotePort === "number" ? socket.remotePort : void 0;
|
|
@@ -986,12 +1373,19 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
986
1373
|
spanAttributes[`http.request.header.${name}`] = attrValue;
|
|
987
1374
|
}
|
|
988
1375
|
span.setAttributes(spanAttributes);
|
|
1376
|
+
record.attributes = Object.assign(attributes, spanAttributes);
|
|
1377
|
+
if (record.llmState) {
|
|
1378
|
+
const requestHeadersObject = {};
|
|
1379
|
+
for (const [key, value] of headersMap.entries()) requestHeadersObject[key] = value;
|
|
1380
|
+
updateLlmRequestHeaders(record.llmState, requestHeadersObject);
|
|
1381
|
+
}
|
|
989
1382
|
}
|
|
990
1383
|
onResponseHeaders({ request, response }) {
|
|
991
1384
|
const record = this._recordFromReq.get(request);
|
|
992
1385
|
if (!record) return;
|
|
993
1386
|
const { span, attributes } = record;
|
|
994
1387
|
const spanAttributes = { [_opentelemetry_semantic_conventions.ATTR_HTTP_RESPONSE_STATUS_CODE]: response.statusCode };
|
|
1388
|
+
const responseHeadersObject = {};
|
|
995
1389
|
const config = this.getConfig();
|
|
996
1390
|
(0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => config.responseHook?.(span, {
|
|
997
1391
|
request,
|
|
@@ -1001,6 +1395,7 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
1001
1395
|
const name = response.headers[idx].toString().toLowerCase();
|
|
1002
1396
|
const value = response.headers[idx + 1];
|
|
1003
1397
|
spanAttributes[`http.response.header.${name}`] = value.toString();
|
|
1398
|
+
responseHeadersObject[name] = value.toString();
|
|
1004
1399
|
if (name === "content-length") {
|
|
1005
1400
|
const contentLength = Number(value.toString());
|
|
1006
1401
|
if (!isNaN(contentLength)) spanAttributes["http.response.header.content-length"] = contentLength;
|
|
@@ -1009,29 +1404,33 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
1009
1404
|
span.setAttributes(spanAttributes);
|
|
1010
1405
|
span.setStatus({ code: response.statusCode >= 400 ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.UNSET });
|
|
1011
1406
|
record.attributes = Object.assign(attributes, spanAttributes);
|
|
1407
|
+
if (record.llmState) updateLlmResponseHeaders(record.llmState, responseHeadersObject);
|
|
1012
1408
|
}
|
|
1013
1409
|
onDone({ request }) {
|
|
1014
1410
|
const record = this._recordFromReq.get(request);
|
|
1015
1411
|
if (!record) return;
|
|
1016
1412
|
const { span, attributes, startTime } = record;
|
|
1413
|
+
const requestContentLength = parseContentLength(record.attributes?.["http.request.header.content-length"]);
|
|
1414
|
+
const effectiveRequestBodySize = requestContentLength !== void 0 ? Math.max(record.requestBodySize, requestContentLength) : record.requestBodySize;
|
|
1415
|
+
const responseContentLength = parseContentLength(record.attributes?.["http.response.header.content-length"]);
|
|
1416
|
+
const effectiveResponseBodySize = responseContentLength !== void 0 ? Math.max(record.responseBodySize, responseContentLength) : record.responseBodySize;
|
|
1417
|
+
span.setAttribute(HTTP_REQUEST_BODY_SIZE, effectiveRequestBodySize);
|
|
1418
|
+
span.setAttribute(HTTP_RESPONSE_BODY_SIZE, effectiveResponseBodySize);
|
|
1017
1419
|
if (shouldCaptureResponseBody(record.url)) {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
span.setAttribute(
|
|
1026
|
-
if (contentEncoding) span.setAttribute(_pingops_core.HTTP_RESPONSE_CONTENT_ENCODING, contentEncoding);
|
|
1027
|
-
} else {
|
|
1028
|
-
const bodyStr = (0, _pingops_core.bufferToBodyString)(responseBodyBuffer);
|
|
1029
|
-
if (bodyStr != null) span.setAttribute(HTTP_RESPONSE_BODY, bodyStr);
|
|
1420
|
+
if (record.responseBodyChunks.length > 0) try {
|
|
1421
|
+
const encoded = encodeBodyBufferForSpan(Buffer.concat(record.responseBodyChunks), {
|
|
1422
|
+
"content-encoding": record.attributes?.["http.response.header.content-encoding"],
|
|
1423
|
+
"content-type": record.attributes?.["http.response.header.content-type"]
|
|
1424
|
+
});
|
|
1425
|
+
if (encoded) {
|
|
1426
|
+
span.setAttribute(HTTP_RESPONSE_BODY, encoded.content);
|
|
1427
|
+
if (encoded.contentEncoding) span.setAttribute(_pingops_core.HTTP_RESPONSE_CONTENT_ENCODING, encoded.contentEncoding);
|
|
1030
1428
|
}
|
|
1031
1429
|
} catch (e) {
|
|
1032
1430
|
this._diag.error("Error occurred while capturing response body:", e);
|
|
1033
1431
|
}
|
|
1034
1432
|
}
|
|
1433
|
+
if (record.llmState) finalizeLlmEnrichment(record.llmState);
|
|
1035
1434
|
span.end();
|
|
1036
1435
|
this._recordFromReq.delete(request);
|
|
1037
1436
|
this.recordRequestDuration(attributes, startTime);
|
|
@@ -1040,10 +1439,19 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
1040
1439
|
const record = this._recordFromReq.get(request);
|
|
1041
1440
|
if (!record) return;
|
|
1042
1441
|
const { span, attributes, startTime } = record;
|
|
1442
|
+
const requestContentLength = parseContentLength(record.attributes?.["http.request.header.content-length"]);
|
|
1443
|
+
const effectiveRequestBodySize = requestContentLength !== void 0 ? Math.max(record.requestBodySize, requestContentLength) : record.requestBodySize;
|
|
1444
|
+
const responseContentLength = parseContentLength(record.attributes?.["http.response.header.content-length"]);
|
|
1445
|
+
const effectiveResponseBodySize = responseContentLength !== void 0 ? Math.max(record.responseBodySize, responseContentLength) : record.responseBodySize;
|
|
1446
|
+
span.setAttribute(HTTP_REQUEST_BODY_SIZE, effectiveRequestBodySize);
|
|
1447
|
+
span.setAttribute(HTTP_RESPONSE_BODY_SIZE, effectiveResponseBodySize);
|
|
1043
1448
|
if (shouldCaptureRequestBody(record.url)) {
|
|
1044
|
-
if (record.requestBodyChunks.length > 0
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1449
|
+
if (record.requestBodyChunks.length > 0) try {
|
|
1450
|
+
const encoded = encodeBodyBufferForSpan(Buffer.concat(record.requestBodyChunks), {
|
|
1451
|
+
"content-encoding": record.attributes?.["http.request.header.content-encoding"],
|
|
1452
|
+
"content-type": record.attributes?.["http.request.header.content-type"]
|
|
1453
|
+
});
|
|
1454
|
+
if (encoded?.content) span.setAttribute(HTTP_REQUEST_BODY, encoded.content);
|
|
1047
1455
|
} catch (e) {
|
|
1048
1456
|
this._diag.error("Error occurred while capturing request body:", e);
|
|
1049
1457
|
}
|
|
@@ -1054,6 +1462,7 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
1054
1462
|
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
1055
1463
|
message: errorMessage
|
|
1056
1464
|
});
|
|
1465
|
+
if (record.llmState) finalizeLlmEnrichment(record.llmState);
|
|
1057
1466
|
span.end();
|
|
1058
1467
|
this._recordFromReq.delete(request);
|
|
1059
1468
|
attributes[_opentelemetry_semantic_conventions.ATTR_ERROR_TYPE] = errorMessage;
|
|
@@ -1062,29 +1471,27 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
1062
1471
|
onBodyChunkSent({ request, chunk }) {
|
|
1063
1472
|
const record = this._recordFromReq.get(request);
|
|
1064
1473
|
if (!record) return;
|
|
1474
|
+
record.requestBodySize += chunk.length;
|
|
1475
|
+
if (record.llmState) appendLlmRequestChunk(record.llmState, chunk);
|
|
1065
1476
|
if (!shouldCaptureRequestBody(record.url)) return;
|
|
1066
|
-
|
|
1067
|
-
if (record.requestBodySize + chunk.length <= maxRequestBodySize) {
|
|
1068
|
-
record.requestBodyChunks.push(chunk);
|
|
1069
|
-
record.requestBodySize += chunk.length;
|
|
1070
|
-
} else {
|
|
1071
|
-
record.requestBodySize = Infinity;
|
|
1072
|
-
record.requestBodyChunks = [];
|
|
1073
|
-
}
|
|
1477
|
+
record.requestBodyChunks.push(chunk);
|
|
1074
1478
|
}
|
|
1075
1479
|
onBodySent({ request }) {
|
|
1076
1480
|
const record = this._recordFromReq.get(request);
|
|
1077
1481
|
if (!record) return;
|
|
1482
|
+
const requestContentLength = parseContentLength(record.attributes?.["http.request.header.content-length"]);
|
|
1483
|
+
const effectiveRequestBodySize = requestContentLength !== void 0 ? Math.max(record.requestBodySize, requestContentLength) : record.requestBodySize;
|
|
1484
|
+
record.span.setAttribute(HTTP_REQUEST_BODY_SIZE, effectiveRequestBodySize);
|
|
1078
1485
|
if (!shouldCaptureRequestBody(record.url)) {
|
|
1079
1486
|
record.requestBodyChunks = [];
|
|
1080
1487
|
return;
|
|
1081
1488
|
}
|
|
1082
|
-
if (record.
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
if (
|
|
1489
|
+
if (record.requestBodyChunks.length > 0) try {
|
|
1490
|
+
const encoded = encodeBodyBufferForSpan(Buffer.concat(record.requestBodyChunks), {
|
|
1491
|
+
"content-encoding": record.attributes?.["http.request.header.content-encoding"],
|
|
1492
|
+
"content-type": record.attributes?.["http.request.header.content-type"]
|
|
1493
|
+
});
|
|
1494
|
+
if (encoded?.content) record.span.setAttribute(HTTP_REQUEST_BODY, encoded.content);
|
|
1088
1495
|
} catch (e) {
|
|
1089
1496
|
this._diag.error("Error occurred while capturing request body:", e);
|
|
1090
1497
|
}
|
|
@@ -1093,15 +1500,10 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
1093
1500
|
onBodyChunkReceived({ request, chunk }) {
|
|
1094
1501
|
const record = this._recordFromReq.get(request);
|
|
1095
1502
|
if (!record) return;
|
|
1503
|
+
record.responseBodySize += chunk.length;
|
|
1504
|
+
if (record.llmState) appendLlmResponseChunk(record.llmState, chunk);
|
|
1096
1505
|
if (!shouldCaptureResponseBody(record.url)) return;
|
|
1097
|
-
|
|
1098
|
-
if (record.responseBodySize + chunk.length <= maxResponseBodySize) {
|
|
1099
|
-
record.responseBodyChunks.push(chunk);
|
|
1100
|
-
record.responseBodySize += chunk.length;
|
|
1101
|
-
} else {
|
|
1102
|
-
record.responseBodySize = Infinity;
|
|
1103
|
-
record.responseBodyChunks = [];
|
|
1104
|
-
}
|
|
1506
|
+
record.responseBodyChunks.push(chunk);
|
|
1105
1507
|
}
|
|
1106
1508
|
recordRequestDuration(attributes, startTime) {
|
|
1107
1509
|
const metricsAttributes = {};
|
|
@@ -1153,14 +1555,11 @@ function toRequestUrl(request) {
|
|
|
1153
1555
|
* @returns UndiciInstrumentation instance
|
|
1154
1556
|
*/
|
|
1155
1557
|
function createUndiciInstrumentation() {
|
|
1156
|
-
const globalConfig$1 = getGlobalConfig();
|
|
1157
1558
|
return new UndiciInstrumentation({
|
|
1158
1559
|
enabled: true,
|
|
1159
1560
|
ignoreRequestHook: (request) => {
|
|
1160
1561
|
return shouldIgnoreOutboundInstrumentation(toRequestUrl(request));
|
|
1161
|
-
}
|
|
1162
|
-
maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
|
|
1163
|
-
maxResponseBodySize: globalConfig$1?.maxResponseBodySize
|
|
1562
|
+
}
|
|
1164
1563
|
});
|
|
1165
1564
|
}
|
|
1166
1565
|
|