@pingops/otel 0.3.0 → 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 +559 -149
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -56
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +18 -56
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +560 -150
- 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];
|
|
@@ -135,31 +136,27 @@ var PingopsSpanProcessor = class {
|
|
|
135
136
|
this.config = {
|
|
136
137
|
debug: config.debug ?? false,
|
|
137
138
|
sdkVersion: config.sdkVersion,
|
|
138
|
-
headersAllowList: config.headersAllowList,
|
|
139
|
-
headersDenyList: config.headersDenyList,
|
|
140
139
|
domainAllowList: config.domainAllowList,
|
|
141
140
|
domainDenyList: config.domainDenyList,
|
|
142
141
|
captureRequestBody: config.captureRequestBody,
|
|
143
142
|
captureResponseBody: config.captureResponseBody,
|
|
144
|
-
|
|
143
|
+
transforms: config.transforms
|
|
145
144
|
};
|
|
146
145
|
setGlobalConfig({
|
|
147
146
|
captureRequestBody: config.captureRequestBody,
|
|
148
147
|
captureResponseBody: config.captureResponseBody,
|
|
149
148
|
domainAllowList: config.domainAllowList,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
exportTraceUrl: this.exporterTraceUrl
|
|
149
|
+
exportTraceUrl: this.exporterTraceUrl,
|
|
150
|
+
llmMonitoring: config.llmMonitoring
|
|
153
151
|
});
|
|
154
|
-
logger$
|
|
152
|
+
logger$3.info("Initialized PingopsSpanProcessor", {
|
|
155
153
|
baseUrl: config.baseUrl,
|
|
156
154
|
exportMode,
|
|
157
155
|
batchSize: config.batchSize,
|
|
158
156
|
batchTimeout: config.batchTimeout,
|
|
159
157
|
hasDomainAllowList: !!config.domainAllowList && config.domainAllowList.length > 0,
|
|
160
158
|
hasDomainDenyList: !!config.domainDenyList && config.domainDenyList.length > 0,
|
|
161
|
-
|
|
162
|
-
hasHeadersDenyList: !!config.headersDenyList && config.headersDenyList.length > 0
|
|
159
|
+
hasTransforms: !!config.transforms
|
|
163
160
|
});
|
|
164
161
|
}
|
|
165
162
|
/**
|
|
@@ -167,7 +164,7 @@ var PingopsSpanProcessor = class {
|
|
|
167
164
|
*/
|
|
168
165
|
onStart(span, parentContext) {
|
|
169
166
|
const spanContext = span.spanContext();
|
|
170
|
-
logger$
|
|
167
|
+
logger$3.debug("Span started", {
|
|
171
168
|
spanName: span.name,
|
|
172
169
|
spanId: spanContext.spanId,
|
|
173
170
|
traceId: spanContext.traceId
|
|
@@ -176,7 +173,7 @@ var PingopsSpanProcessor = class {
|
|
|
176
173
|
const propagatedAttributes = (0, _pingops_core.getPropagatedAttributesFromContext)(parentContext);
|
|
177
174
|
if (Object.keys(propagatedAttributes).length > 0) {
|
|
178
175
|
for (const [key, value] of Object.entries(propagatedAttributes)) if (typeof value === "string" || Array.isArray(value)) span.setAttribute(key, value);
|
|
179
|
-
logger$
|
|
176
|
+
logger$3.debug("Set propagated attributes on span", {
|
|
180
177
|
spanName: span.name,
|
|
181
178
|
attributeKeys: Object.keys(propagatedAttributes)
|
|
182
179
|
});
|
|
@@ -194,7 +191,7 @@ var PingopsSpanProcessor = class {
|
|
|
194
191
|
*/
|
|
195
192
|
onEnd(span) {
|
|
196
193
|
const spanContext = span.spanContext();
|
|
197
|
-
logger$
|
|
194
|
+
logger$3.debug("Span ended, processing", {
|
|
198
195
|
spanName: span.name,
|
|
199
196
|
spanId: spanContext.spanId,
|
|
200
197
|
traceId: spanContext.traceId,
|
|
@@ -202,7 +199,7 @@ var PingopsSpanProcessor = class {
|
|
|
202
199
|
});
|
|
203
200
|
try {
|
|
204
201
|
if (!(0, _pingops_core.isSpanEligible)(span)) {
|
|
205
|
-
logger$
|
|
202
|
+
logger$3.debug("Span not eligible, skipping", {
|
|
206
203
|
spanName: span.name,
|
|
207
204
|
spanId: spanContext.spanId,
|
|
208
205
|
reason: "not CLIENT or missing HTTP/GenAI attributes"
|
|
@@ -212,14 +209,14 @@ var PingopsSpanProcessor = class {
|
|
|
212
209
|
const attributes = span.attributes;
|
|
213
210
|
const url$1 = (0, _pingops_core.getHttpUrlFromAttributes)(attributes) ?? "";
|
|
214
211
|
if (url$1 && isExporterRequestUrl$1(url$1, this.exporterTraceUrl)) {
|
|
215
|
-
logger$
|
|
212
|
+
logger$3.debug("Skipping exporter span to prevent self-instrumentation", {
|
|
216
213
|
spanName: span.name,
|
|
217
214
|
spanId: spanContext.spanId,
|
|
218
215
|
url: url$1
|
|
219
216
|
});
|
|
220
217
|
return;
|
|
221
218
|
}
|
|
222
|
-
logger$
|
|
219
|
+
logger$3.debug("Extracted URL for domain filtering", {
|
|
223
220
|
spanName: span.name,
|
|
224
221
|
url: url$1,
|
|
225
222
|
hasHttpUrl: !!attributes["http.url"],
|
|
@@ -228,25 +225,25 @@ var PingopsSpanProcessor = class {
|
|
|
228
225
|
});
|
|
229
226
|
if (url$1) {
|
|
230
227
|
if (!(0, _pingops_core.shouldCaptureSpan)(url$1, this.config.domainAllowList, this.config.domainDenyList)) {
|
|
231
|
-
logger$
|
|
228
|
+
logger$3.info("Span filtered out by domain rules", {
|
|
232
229
|
spanName: span.name,
|
|
233
230
|
spanId: spanContext.spanId,
|
|
234
231
|
url: url$1
|
|
235
232
|
});
|
|
236
233
|
return;
|
|
237
234
|
}
|
|
238
|
-
} else logger$
|
|
239
|
-
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);
|
|
240
237
|
this.processor.onEnd(filteredSpan);
|
|
241
|
-
logger$
|
|
238
|
+
logger$3.info("Span passed all filters and queued for export", {
|
|
242
239
|
spanName: span.name,
|
|
243
240
|
spanId: spanContext.spanId,
|
|
244
241
|
traceId: spanContext.traceId,
|
|
245
242
|
url: url$1,
|
|
246
|
-
|
|
243
|
+
hasTransforms: !!this.config.transforms
|
|
247
244
|
});
|
|
248
245
|
} catch (error) {
|
|
249
|
-
logger$
|
|
246
|
+
logger$3.error("Error processing span", {
|
|
250
247
|
spanName: span.name,
|
|
251
248
|
spanId: spanContext.spanId,
|
|
252
249
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -259,12 +256,12 @@ var PingopsSpanProcessor = class {
|
|
|
259
256
|
* @returns Promise that resolves when all pending operations are complete
|
|
260
257
|
*/
|
|
261
258
|
async forceFlush() {
|
|
262
|
-
logger$
|
|
259
|
+
logger$3.info("Force flushing spans");
|
|
263
260
|
try {
|
|
264
261
|
await this.processor.forceFlush();
|
|
265
|
-
logger$
|
|
262
|
+
logger$3.info("Force flush complete");
|
|
266
263
|
} catch (error) {
|
|
267
|
-
logger$
|
|
264
|
+
logger$3.error("Error during force flush", { error: error instanceof Error ? error.message : String(error) });
|
|
268
265
|
throw error;
|
|
269
266
|
}
|
|
270
267
|
}
|
|
@@ -274,12 +271,12 @@ var PingopsSpanProcessor = class {
|
|
|
274
271
|
* @returns Promise that resolves when shutdown is complete
|
|
275
272
|
*/
|
|
276
273
|
async shutdown() {
|
|
277
|
-
logger$
|
|
274
|
+
logger$3.info("Shutting down processor");
|
|
278
275
|
try {
|
|
279
276
|
await this.processor.shutdown();
|
|
280
|
-
logger$
|
|
277
|
+
logger$3.info("Processor shutdown complete");
|
|
281
278
|
} catch (error) {
|
|
282
|
-
logger$
|
|
279
|
+
logger$3.error("Error during processor shutdown", { error: error instanceof Error ? error.message : String(error) });
|
|
283
280
|
throw error;
|
|
284
281
|
}
|
|
285
282
|
}
|
|
@@ -294,7 +291,7 @@ const PINGOPS_GLOBAL_SYMBOL = Symbol.for("pingops");
|
|
|
294
291
|
/**
|
|
295
292
|
* Logger instance for tracer provider
|
|
296
293
|
*/
|
|
297
|
-
const logger$
|
|
294
|
+
const logger$2 = (0, _pingops_core.createLogger)("[PingOps TracerProvider]");
|
|
298
295
|
/**
|
|
299
296
|
* Creates initial global state
|
|
300
297
|
*/
|
|
@@ -309,21 +306,21 @@ function getGlobalState() {
|
|
|
309
306
|
try {
|
|
310
307
|
const g = globalThis;
|
|
311
308
|
if (typeof g !== "object" || g === null) {
|
|
312
|
-
logger$
|
|
309
|
+
logger$2.warn("globalThis is not available, using fallback state");
|
|
313
310
|
return initialState;
|
|
314
311
|
}
|
|
315
312
|
if (!g[PINGOPS_GLOBAL_SYMBOL]) {
|
|
316
|
-
logger$
|
|
313
|
+
logger$2.debug("Creating new global state");
|
|
317
314
|
Object.defineProperty(g, PINGOPS_GLOBAL_SYMBOL, {
|
|
318
315
|
value: initialState,
|
|
319
316
|
writable: false,
|
|
320
317
|
configurable: false,
|
|
321
318
|
enumerable: false
|
|
322
319
|
});
|
|
323
|
-
} else logger$
|
|
320
|
+
} else logger$2.debug("Retrieved existing global state");
|
|
324
321
|
return g[PINGOPS_GLOBAL_SYMBOL];
|
|
325
322
|
} catch (err) {
|
|
326
|
-
logger$
|
|
323
|
+
logger$2.error("Failed to access global state:", err instanceof Error ? err.message : String(err));
|
|
327
324
|
return initialState;
|
|
328
325
|
}
|
|
329
326
|
}
|
|
@@ -341,11 +338,11 @@ function setPingopsTracerProvider(provider) {
|
|
|
341
338
|
const state = getGlobalState();
|
|
342
339
|
const hadProvider = state.isolatedTracerProvider !== null;
|
|
343
340
|
state.isolatedTracerProvider = provider;
|
|
344
|
-
if (provider) logger$
|
|
341
|
+
if (provider) logger$2.info("Set isolated TracerProvider", {
|
|
345
342
|
hadPrevious: hadProvider,
|
|
346
343
|
providerType: provider.constructor.name
|
|
347
344
|
});
|
|
348
|
-
else logger$
|
|
345
|
+
else logger$2.info("Cleared isolated TracerProvider", { hadPrevious: hadProvider });
|
|
349
346
|
}
|
|
350
347
|
/**
|
|
351
348
|
* Gets the TracerProvider for PingOps tracing operations.
|
|
@@ -359,36 +356,36 @@ function setPingopsTracerProvider(provider) {
|
|
|
359
356
|
function getPingopsTracerProvider() {
|
|
360
357
|
const { isolatedTracerProvider } = getGlobalState();
|
|
361
358
|
if (isolatedTracerProvider) {
|
|
362
|
-
logger$
|
|
359
|
+
logger$2.debug("Using isolated TracerProvider", { providerType: isolatedTracerProvider.constructor.name });
|
|
363
360
|
return isolatedTracerProvider;
|
|
364
361
|
}
|
|
365
362
|
const globalProvider = _opentelemetry_api.trace.getTracerProvider();
|
|
366
|
-
logger$
|
|
363
|
+
logger$2.debug("Using global TracerProvider", { providerType: globalProvider.constructor.name });
|
|
367
364
|
return globalProvider;
|
|
368
365
|
}
|
|
369
366
|
/**
|
|
370
367
|
* Shuts down the TracerProvider and flushes remaining spans
|
|
371
368
|
*/
|
|
372
369
|
async function shutdownTracerProvider() {
|
|
373
|
-
logger$
|
|
370
|
+
logger$2.info("Shutting down TracerProvider");
|
|
374
371
|
const providerWithShutdown = getPingopsTracerProvider();
|
|
375
372
|
if (providerWithShutdown && typeof providerWithShutdown.shutdown === "function") {
|
|
376
|
-
logger$
|
|
373
|
+
logger$2.debug("Calling provider.shutdown()");
|
|
377
374
|
try {
|
|
378
375
|
await providerWithShutdown.shutdown();
|
|
379
|
-
logger$
|
|
376
|
+
logger$2.info("TracerProvider shutdown complete");
|
|
380
377
|
} catch (error) {
|
|
381
|
-
logger$
|
|
378
|
+
logger$2.error("Error during TracerProvider shutdown:", error instanceof Error ? error.message : String(error));
|
|
382
379
|
throw error;
|
|
383
380
|
}
|
|
384
|
-
} else logger$
|
|
381
|
+
} else logger$2.warn("TracerProvider does not have shutdown method, skipping");
|
|
385
382
|
setPingopsTracerProvider(null);
|
|
386
|
-
logger$
|
|
383
|
+
logger$2.info("TracerProvider shutdown finished");
|
|
387
384
|
}
|
|
388
385
|
|
|
389
386
|
//#endregion
|
|
390
387
|
//#region src/instrumentations/suppression-guard.ts
|
|
391
|
-
const logger = (0, _pingops_core.createLogger)("[PingOps SuppressionGuard]");
|
|
388
|
+
const logger$1 = (0, _pingops_core.createLogger)("[PingOps SuppressionGuard]");
|
|
392
389
|
let hasLoggedSuppressionLeakWarning = false;
|
|
393
390
|
function normalizePath(pathname) {
|
|
394
391
|
return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
@@ -428,9 +425,9 @@ function resolveOutboundSpanParentContext(activeContext, requestUrl) {
|
|
|
428
425
|
if (activeContext.getValue(_pingops_core.PINGOPS_INTENTIONAL_SUPPRESSION) === true) return activeContext;
|
|
429
426
|
if (isExporterRequestUrl(requestUrl)) return activeContext;
|
|
430
427
|
if (!hasLoggedSuppressionLeakWarning) {
|
|
431
|
-
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");
|
|
432
429
|
hasLoggedSuppressionLeakWarning = true;
|
|
433
|
-
} 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");
|
|
434
431
|
return _opentelemetry_api.ROOT_CONTEXT;
|
|
435
432
|
}
|
|
436
433
|
|
|
@@ -440,8 +437,32 @@ const HTTP_REQUEST_BODY = "http.request.body";
|
|
|
440
437
|
const HTTP_RESPONSE_BODY = "http.response.body";
|
|
441
438
|
const HTTP_REQUEST_BODY_SIZE = "http.request.body.size";
|
|
442
439
|
const HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";
|
|
443
|
-
const
|
|
444
|
-
const
|
|
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
|
+
]);
|
|
445
466
|
/**
|
|
446
467
|
* Gets domain rule configuration for a given URL.
|
|
447
468
|
*/
|
|
@@ -489,6 +510,395 @@ function toBufferChunk(data) {
|
|
|
489
510
|
if (data instanceof Uint8Array) return Buffer.from(data);
|
|
490
511
|
return null;
|
|
491
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
|
+
}
|
|
492
902
|
|
|
493
903
|
//#endregion
|
|
494
904
|
//#region src/instrumentations/http/pingops-http.ts
|
|
@@ -535,9 +945,9 @@ function setAttributeValue(span, attrName, attrValue) {
|
|
|
535
945
|
/**
|
|
536
946
|
* Captures request body from a chunk buffer.
|
|
537
947
|
*/
|
|
538
|
-
function captureRequestBody(span, data,
|
|
948
|
+
function captureRequestBody(span, data, semanticAttr, url$1) {
|
|
539
949
|
if (!shouldCaptureRequestBody(url$1)) return;
|
|
540
|
-
if (data.length
|
|
950
|
+
if (data.length) try {
|
|
541
951
|
const requestBody = data.toString("utf-8");
|
|
542
952
|
if (requestBody) setAttributeValue(span, semanticAttr, requestBody);
|
|
543
953
|
} catch (e) {
|
|
@@ -547,25 +957,13 @@ function captureRequestBody(span, data, maxSize, semanticAttr, url$1) {
|
|
|
547
957
|
/**
|
|
548
958
|
* Captures response body from chunks
|
|
549
959
|
*/
|
|
550
|
-
function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url$1
|
|
960
|
+
function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url$1) {
|
|
551
961
|
if (!shouldCaptureResponseBody(url$1)) return;
|
|
552
|
-
if (chunks === null) {
|
|
553
|
-
const contentEncoding = responseHeaders?.["content-encoding"];
|
|
554
|
-
const contentType = responseHeaders?.["content-type"];
|
|
555
|
-
const toHeaderString = (value) => typeof value === "string" ? value : Array.isArray(value) ? value.join(", ") : "unknown";
|
|
556
|
-
setAttributeValue(span, semanticAttr, `[truncated response body; exceeded maxResponseBodySize=${maxSize ?? DEFAULT_MAX_RESPONSE_BODY_SIZE}; content-type=${toHeaderString(contentType)}; content-encoding=${toHeaderString(contentEncoding)}]`);
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
962
|
if (chunks.length) try {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
setAttributeValue(span,
|
|
564
|
-
const encStr = typeof contentEncoding === "string" ? contentEncoding : Array.isArray(contentEncoding) ? contentEncoding.map(String).join(", ") : void 0;
|
|
565
|
-
if (encStr) setAttributeValue(span, _pingops_core.HTTP_RESPONSE_CONTENT_ENCODING, encStr);
|
|
566
|
-
} else {
|
|
567
|
-
const bodyStr = (0, _pingops_core.bufferToBodyString)(concatedChunks);
|
|
568
|
-
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);
|
|
569
967
|
}
|
|
570
968
|
} catch (e) {
|
|
571
969
|
console.error("Error occurred while capturing response body:", e);
|
|
@@ -642,6 +1040,7 @@ function extractClientRequestPath(request) {
|
|
|
642
1040
|
}
|
|
643
1041
|
const PingopsHttpSemanticAttributes = PingopsSemanticAttributes;
|
|
644
1042
|
var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_http.HttpInstrumentation {
|
|
1043
|
+
_llmStateByRequest = /* @__PURE__ */ new WeakMap();
|
|
645
1044
|
constructor(config) {
|
|
646
1045
|
super(config);
|
|
647
1046
|
this._config = this._createConfig(config);
|
|
@@ -663,21 +1062,26 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
663
1062
|
_createConfig(config) {
|
|
664
1063
|
return {
|
|
665
1064
|
...config,
|
|
666
|
-
requestHook: this._createRequestHook(config?.requestHook
|
|
667
|
-
responseHook: this._createResponseHook(config?.responseHook
|
|
1065
|
+
requestHook: this._createRequestHook(config?.requestHook),
|
|
1066
|
+
responseHook: this._createResponseHook(config?.responseHook)
|
|
668
1067
|
};
|
|
669
1068
|
}
|
|
670
|
-
_createRequestHook(originalRequestHook
|
|
1069
|
+
_createRequestHook(originalRequestHook) {
|
|
671
1070
|
return (span, request) => {
|
|
672
1071
|
const headers = extractRequestHeaders(request);
|
|
673
1072
|
if (headers) captureRequestHeaders(span, headers);
|
|
674
1073
|
if (request instanceof http.ClientRequest) {
|
|
675
|
-
const maxRequestBodySize = config?.maxRequestBodySize || DEFAULT_MAX_REQUEST_BODY_SIZE;
|
|
676
1074
|
let requestBodySize = 0;
|
|
677
|
-
|
|
1075
|
+
const requestContentLength = parseContentLength(headers?.["content-length"]);
|
|
1076
|
+
span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestContentLength !== void 0 ? Math.max(requestBodySize, requestContentLength) : requestBodySize);
|
|
678
1077
|
const hostHeader = request.getHeader("host");
|
|
679
1078
|
const host = typeof hostHeader === "string" ? hostHeader : Array.isArray(hostHeader) ? hostHeader.join(",") : typeof hostHeader === "number" ? String(hostHeader) : void 0;
|
|
680
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
|
+
}
|
|
681
1085
|
if (typeof request.path === "string" && request.path.length > 0) {
|
|
682
1086
|
const { path, query } = parseRequestPathAndQuery(request.path);
|
|
683
1087
|
span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_PATH, path);
|
|
@@ -690,8 +1094,9 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
690
1094
|
const chunkBuffer = toBufferChunk(data);
|
|
691
1095
|
if (chunkBuffer) {
|
|
692
1096
|
requestBodySize += chunkBuffer.length;
|
|
693
|
-
span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestBodySize);
|
|
694
|
-
captureRequestBody(span, chunkBuffer,
|
|
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);
|
|
695
1100
|
}
|
|
696
1101
|
return originalWrite(data, ...rest);
|
|
697
1102
|
});
|
|
@@ -699,8 +1104,9 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
699
1104
|
const chunkBuffer = toBufferChunk(data);
|
|
700
1105
|
if (chunkBuffer) {
|
|
701
1106
|
requestBodySize += chunkBuffer.length;
|
|
702
|
-
span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestBodySize);
|
|
703
|
-
captureRequestBody(span, chunkBuffer,
|
|
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);
|
|
704
1110
|
}
|
|
705
1111
|
return originalEnd(data, ...rest);
|
|
706
1112
|
});
|
|
@@ -708,39 +1114,45 @@ var PingopsHttpInstrumentation = class extends _opentelemetry_instrumentation_ht
|
|
|
708
1114
|
if (originalRequestHook) originalRequestHook(span, request);
|
|
709
1115
|
};
|
|
710
1116
|
}
|
|
711
|
-
_createResponseHook(originalResponseHook
|
|
1117
|
+
_createResponseHook(originalResponseHook) {
|
|
712
1118
|
return (span, response) => {
|
|
713
1119
|
const headers = extractResponseHeaders(response);
|
|
714
1120
|
if (headers) captureResponseHeaders(span, headers);
|
|
715
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;
|
|
716
1124
|
const requestPath = response.req instanceof http.ClientRequest ? extractClientRequestPath(response.req) : void 0;
|
|
717
1125
|
if (requestPath) {
|
|
718
1126
|
const { path, query } = parseRequestPathAndQuery(requestPath);
|
|
719
1127
|
span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_PATH, path);
|
|
720
1128
|
if (query) span.setAttribute(_opentelemetry_semantic_conventions.ATTR_URL_QUERY, query);
|
|
721
1129
|
}
|
|
722
|
-
const maxResponseBodySize = config?.maxResponseBodySize || DEFAULT_MAX_RESPONSE_BODY_SIZE;
|
|
723
1130
|
const url$1 = response.url || void 0;
|
|
724
|
-
|
|
1131
|
+
const chunks = [];
|
|
725
1132
|
let totalSize = 0;
|
|
726
1133
|
span.setAttribute(PingopsSemanticAttributes.HTTP_RESPONSE_BODY_SIZE, 0);
|
|
727
1134
|
const shouldCapture = shouldCaptureResponseBody(url$1);
|
|
1135
|
+
if (llmState && headers) updateLlmResponseHeaders(llmState, headers);
|
|
728
1136
|
response.prependListener("data", (chunk) => {
|
|
729
1137
|
if (!chunk) return;
|
|
730
1138
|
const chunkBuffer = toBufferChunk(chunk);
|
|
731
1139
|
if (!chunkBuffer) return;
|
|
732
1140
|
totalSize += chunkBuffer.length;
|
|
733
1141
|
span.setAttribute(PingopsSemanticAttributes.HTTP_RESPONSE_BODY_SIZE, totalSize);
|
|
1142
|
+
if (llmState) appendLlmResponseChunk(llmState, chunkBuffer);
|
|
734
1143
|
if (!shouldCapture) return;
|
|
735
|
-
|
|
736
|
-
else chunks = null;
|
|
1144
|
+
chunks.push(chunkBuffer);
|
|
737
1145
|
});
|
|
738
1146
|
let finalized = false;
|
|
739
1147
|
const finalizeCapture = () => {
|
|
740
1148
|
if (finalized) return;
|
|
741
1149
|
finalized = true;
|
|
742
|
-
|
|
743
|
-
|
|
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);
|
|
744
1156
|
};
|
|
745
1157
|
response.prependOnceListener("end", finalizeCapture);
|
|
746
1158
|
response.prependOnceListener("close", finalizeCapture);
|
|
@@ -772,7 +1184,6 @@ function toRequestUrl$1(request) {
|
|
|
772
1184
|
* @returns PingopsHttpInstrumentation instance
|
|
773
1185
|
*/
|
|
774
1186
|
function createHttpInstrumentation(config) {
|
|
775
|
-
const globalConfig$1 = getGlobalConfig();
|
|
776
1187
|
const userIgnoreOutgoingRequestHook = config?.ignoreOutgoingRequestHook;
|
|
777
1188
|
return new PingopsHttpInstrumentation({
|
|
778
1189
|
...config,
|
|
@@ -780,9 +1191,7 @@ function createHttpInstrumentation(config) {
|
|
|
780
1191
|
ignoreOutgoingRequestHook: (request) => {
|
|
781
1192
|
if (shouldIgnoreOutboundInstrumentation(toRequestUrl$1(request))) return true;
|
|
782
1193
|
return userIgnoreOutgoingRequestHook?.(request) ?? false;
|
|
783
|
-
}
|
|
784
|
-
maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
|
|
785
|
-
maxResponseBodySize: globalConfig$1?.maxResponseBodySize
|
|
1194
|
+
}
|
|
786
1195
|
});
|
|
787
1196
|
}
|
|
788
1197
|
|
|
@@ -907,7 +1316,10 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
907
1316
|
const serverPort = requestUrl.port || schemePorts[urlScheme];
|
|
908
1317
|
attributes[_opentelemetry_semantic_conventions.ATTR_SERVER_ADDRESS] = serverAddress;
|
|
909
1318
|
if (serverPort && !isNaN(Number(serverPort))) attributes[_opentelemetry_semantic_conventions.ATTR_SERVER_PORT] = Number(serverPort);
|
|
910
|
-
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");
|
|
911
1323
|
if (userAgentValues) attributes[_opentelemetry_semantic_conventions.ATTR_USER_AGENT_ORIGINAL] = Array.isArray(userAgentValues) ? userAgentValues[userAgentValues.length - 1] : userAgentValues;
|
|
912
1324
|
const hookAttributes = (0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => config.startSpanHook?.(request), (e) => e && this._diag.error("caught startSpanHook error: ", e), true);
|
|
913
1325
|
if (hookAttributes) Object.entries(hookAttributes).forEach(([key, val]) => {
|
|
@@ -940,17 +1352,16 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
940
1352
|
responseBodyChunks: [],
|
|
941
1353
|
requestBodySize: 0,
|
|
942
1354
|
responseBodySize: 0,
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
requestBodyCaptureExceeded: false,
|
|
946
|
-
responseBodyCaptureExceeded: false,
|
|
947
|
-
url: requestUrl.toString()
|
|
1355
|
+
url: requestUrl.toString(),
|
|
1356
|
+
llmState: createLlmEnrichmentState(span, requestUrl.toString(), request.method, requestHeadersObject)
|
|
948
1357
|
});
|
|
1358
|
+
const createdRecord = this._recordFromReq.get(request);
|
|
1359
|
+
if (createdRecord?.llmState) updateLlmRequestHeaders(createdRecord.llmState, requestHeadersObject);
|
|
949
1360
|
}
|
|
950
1361
|
onRequestHeaders({ request, socket }) {
|
|
951
1362
|
const record = this._recordFromReq.get(request);
|
|
952
1363
|
if (!record) return;
|
|
953
|
-
const { span } = record;
|
|
1364
|
+
const { span, attributes } = record;
|
|
954
1365
|
const spanAttributes = {};
|
|
955
1366
|
const remoteAddress = typeof socket.remoteAddress === "string" ? socket.remoteAddress : void 0;
|
|
956
1367
|
const remotePort = typeof socket.remotePort === "number" ? socket.remotePort : void 0;
|
|
@@ -962,12 +1373,19 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
962
1373
|
spanAttributes[`http.request.header.${name}`] = attrValue;
|
|
963
1374
|
}
|
|
964
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
|
+
}
|
|
965
1382
|
}
|
|
966
1383
|
onResponseHeaders({ request, response }) {
|
|
967
1384
|
const record = this._recordFromReq.get(request);
|
|
968
1385
|
if (!record) return;
|
|
969
1386
|
const { span, attributes } = record;
|
|
970
1387
|
const spanAttributes = { [_opentelemetry_semantic_conventions.ATTR_HTTP_RESPONSE_STATUS_CODE]: response.statusCode };
|
|
1388
|
+
const responseHeadersObject = {};
|
|
971
1389
|
const config = this.getConfig();
|
|
972
1390
|
(0, _opentelemetry_instrumentation.safeExecuteInTheMiddle)(() => config.responseHook?.(span, {
|
|
973
1391
|
request,
|
|
@@ -977,6 +1395,7 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
977
1395
|
const name = response.headers[idx].toString().toLowerCase();
|
|
978
1396
|
const value = response.headers[idx + 1];
|
|
979
1397
|
spanAttributes[`http.response.header.${name}`] = value.toString();
|
|
1398
|
+
responseHeadersObject[name] = value.toString();
|
|
980
1399
|
if (name === "content-length") {
|
|
981
1400
|
const contentLength = Number(value.toString());
|
|
982
1401
|
if (!isNaN(contentLength)) spanAttributes["http.response.header.content-length"] = contentLength;
|
|
@@ -985,31 +1404,33 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
985
1404
|
span.setAttributes(spanAttributes);
|
|
986
1405
|
span.setStatus({ code: response.statusCode >= 400 ? _opentelemetry_api.SpanStatusCode.ERROR : _opentelemetry_api.SpanStatusCode.UNSET });
|
|
987
1406
|
record.attributes = Object.assign(attributes, spanAttributes);
|
|
1407
|
+
if (record.llmState) updateLlmResponseHeaders(record.llmState, responseHeadersObject);
|
|
988
1408
|
}
|
|
989
1409
|
onDone({ request }) {
|
|
990
1410
|
const record = this._recordFromReq.get(request);
|
|
991
1411
|
if (!record) return;
|
|
992
1412
|
const { span, attributes, startTime } = record;
|
|
993
|
-
|
|
994
|
-
|
|
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);
|
|
995
1419
|
if (shouldCaptureResponseBody(record.url)) {
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
span.setAttribute(
|
|
1004
|
-
if (contentEncoding) span.setAttribute(_pingops_core.HTTP_RESPONSE_CONTENT_ENCODING, contentEncoding);
|
|
1005
|
-
} else {
|
|
1006
|
-
const bodyStr = (0, _pingops_core.bufferToBodyString)(responseBodyBuffer);
|
|
1007
|
-
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);
|
|
1008
1428
|
}
|
|
1009
1429
|
} catch (e) {
|
|
1010
1430
|
this._diag.error("Error occurred while capturing response body:", e);
|
|
1011
1431
|
}
|
|
1012
1432
|
}
|
|
1433
|
+
if (record.llmState) finalizeLlmEnrichment(record.llmState);
|
|
1013
1434
|
span.end();
|
|
1014
1435
|
this._recordFromReq.delete(request);
|
|
1015
1436
|
this.recordRequestDuration(attributes, startTime);
|
|
@@ -1018,12 +1439,19 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
1018
1439
|
const record = this._recordFromReq.get(request);
|
|
1019
1440
|
if (!record) return;
|
|
1020
1441
|
const { span, attributes, startTime } = record;
|
|
1021
|
-
|
|
1022
|
-
|
|
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);
|
|
1023
1448
|
if (shouldCaptureRequestBody(record.url)) {
|
|
1024
|
-
if (record.requestBodyChunks.length > 0
|
|
1025
|
-
const
|
|
1026
|
-
|
|
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);
|
|
1027
1455
|
} catch (e) {
|
|
1028
1456
|
this._diag.error("Error occurred while capturing request body:", e);
|
|
1029
1457
|
}
|
|
@@ -1034,6 +1462,7 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
1034
1462
|
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
1035
1463
|
message: errorMessage
|
|
1036
1464
|
});
|
|
1465
|
+
if (record.llmState) finalizeLlmEnrichment(record.llmState);
|
|
1037
1466
|
span.end();
|
|
1038
1467
|
this._recordFromReq.delete(request);
|
|
1039
1468
|
attributes[_opentelemetry_semantic_conventions.ATTR_ERROR_TYPE] = errorMessage;
|
|
@@ -1043,54 +1472,38 @@ var UndiciInstrumentation = class extends _opentelemetry_instrumentation.Instrum
|
|
|
1043
1472
|
const record = this._recordFromReq.get(request);
|
|
1044
1473
|
if (!record) return;
|
|
1045
1474
|
record.requestBodySize += chunk.length;
|
|
1475
|
+
if (record.llmState) appendLlmRequestChunk(record.llmState, chunk);
|
|
1046
1476
|
if (!shouldCaptureRequestBody(record.url)) return;
|
|
1047
|
-
|
|
1048
|
-
if (!record.requestBodyCaptureExceeded && record.requestBodyCaptureSize + chunk.length <= maxRequestBodySize) {
|
|
1049
|
-
record.requestBodyChunks.push(chunk);
|
|
1050
|
-
record.requestBodyCaptureSize += chunk.length;
|
|
1051
|
-
} else {
|
|
1052
|
-
record.requestBodyCaptureExceeded = true;
|
|
1053
|
-
record.requestBodyChunks = [];
|
|
1054
|
-
record.requestBodyCaptureSize = 0;
|
|
1055
|
-
}
|
|
1477
|
+
record.requestBodyChunks.push(chunk);
|
|
1056
1478
|
}
|
|
1057
1479
|
onBodySent({ request }) {
|
|
1058
1480
|
const record = this._recordFromReq.get(request);
|
|
1059
1481
|
if (!record) return;
|
|
1060
|
-
record.
|
|
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);
|
|
1061
1485
|
if (!shouldCaptureRequestBody(record.url)) {
|
|
1062
1486
|
record.requestBodyChunks = [];
|
|
1063
|
-
record.requestBodyCaptureSize = 0;
|
|
1064
|
-
record.requestBodyCaptureExceeded = false;
|
|
1065
1487
|
return;
|
|
1066
1488
|
}
|
|
1067
|
-
if (record.
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
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);
|
|
1073
1495
|
} catch (e) {
|
|
1074
1496
|
this._diag.error("Error occurred while capturing request body:", e);
|
|
1075
1497
|
}
|
|
1076
1498
|
record.requestBodyChunks = [];
|
|
1077
|
-
record.requestBodyCaptureSize = 0;
|
|
1078
|
-
record.requestBodyCaptureExceeded = false;
|
|
1079
1499
|
}
|
|
1080
1500
|
onBodyChunkReceived({ request, chunk }) {
|
|
1081
1501
|
const record = this._recordFromReq.get(request);
|
|
1082
1502
|
if (!record) return;
|
|
1083
1503
|
record.responseBodySize += chunk.length;
|
|
1504
|
+
if (record.llmState) appendLlmResponseChunk(record.llmState, chunk);
|
|
1084
1505
|
if (!shouldCaptureResponseBody(record.url)) return;
|
|
1085
|
-
|
|
1086
|
-
if (!record.responseBodyCaptureExceeded && record.responseBodyCaptureSize + chunk.length <= maxResponseBodySize) {
|
|
1087
|
-
record.responseBodyChunks.push(chunk);
|
|
1088
|
-
record.responseBodyCaptureSize += chunk.length;
|
|
1089
|
-
} else {
|
|
1090
|
-
record.responseBodyCaptureExceeded = true;
|
|
1091
|
-
record.responseBodyChunks = [];
|
|
1092
|
-
record.responseBodyCaptureSize = 0;
|
|
1093
|
-
}
|
|
1506
|
+
record.responseBodyChunks.push(chunk);
|
|
1094
1507
|
}
|
|
1095
1508
|
recordRequestDuration(attributes, startTime) {
|
|
1096
1509
|
const metricsAttributes = {};
|
|
@@ -1142,14 +1555,11 @@ function toRequestUrl(request) {
|
|
|
1142
1555
|
* @returns UndiciInstrumentation instance
|
|
1143
1556
|
*/
|
|
1144
1557
|
function createUndiciInstrumentation() {
|
|
1145
|
-
const globalConfig$1 = getGlobalConfig();
|
|
1146
1558
|
return new UndiciInstrumentation({
|
|
1147
1559
|
enabled: true,
|
|
1148
1560
|
ignoreRequestHook: (request) => {
|
|
1149
1561
|
return shouldIgnoreOutboundInstrumentation(toRequestUrl(request));
|
|
1150
|
-
}
|
|
1151
|
-
maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
|
|
1152
|
-
maxResponseBodySize: globalConfig$1?.maxResponseBodySize
|
|
1562
|
+
}
|
|
1153
1563
|
});
|
|
1154
1564
|
}
|
|
1155
1565
|
|