@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.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BatchSpanProcessor, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
2
2
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
3
|
-
import { HTTP_RESPONSE_CONTENT_ENCODING, PINGOPS_CAPTURE_REQUEST_BODY, PINGOPS_CAPTURE_RESPONSE_BODY, PINGOPS_INTENTIONAL_SUPPRESSION,
|
|
3
|
+
import { HTTP_RESPONSE_CONTENT_ENCODING, PINGOPS_CAPTURE_REQUEST_BODY, PINGOPS_CAPTURE_RESPONSE_BODY, PINGOPS_INTENTIONAL_SUPPRESSION, createLogger, extractDomainFromUrl, extractSpanPayload, getHttpUrlFromAttributes, getPropagatedAttributesFromContext, isCompressedContentEncoding, isSpanEligible, shouldCaptureSpan } from "@pingops/core";
|
|
4
4
|
import { INVALID_SPAN_CONTEXT, ROOT_CONTEXT, SpanKind, SpanStatusCode, ValueType, context, propagation, trace } from "@opentelemetry/api";
|
|
5
5
|
import "@opentelemetry/sdk-trace-node";
|
|
6
6
|
import "@opentelemetry/resources";
|
|
@@ -8,6 +8,7 @@ import { ATTR_ERROR_TYPE, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_REQUEST_METHOD_ORI
|
|
|
8
8
|
import { ClientRequest, IncomingMessage } from "http";
|
|
9
9
|
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
|
|
10
10
|
import { hrTime, hrTimeDuration, hrTimeToMilliseconds, isTracingSuppressed } from "@opentelemetry/core";
|
|
11
|
+
import { brotliDecompressSync, gunzipSync, inflateSync } from "zlib";
|
|
11
12
|
import * as diagch from "diagnostics_channel";
|
|
12
13
|
import { URL as URL$1 } from "url";
|
|
13
14
|
import { InstrumentationBase, safeExecuteInTheMiddle } from "@opentelemetry/instrumentation";
|
|
@@ -31,7 +32,7 @@ function getGlobalConfig() {
|
|
|
31
32
|
|
|
32
33
|
//#endregion
|
|
33
34
|
//#region src/span-processor.ts
|
|
34
|
-
const logger$
|
|
35
|
+
const logger$3 = createLogger("[PingOps Processor]");
|
|
35
36
|
function normalizePath$1(pathname) {
|
|
36
37
|
return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
37
38
|
}
|
|
@@ -61,10 +62,10 @@ function isExporterRequestUrl$1(url, exporterUrl) {
|
|
|
61
62
|
*
|
|
62
63
|
* This allows us to filter headers before the span is serialized by OTLP exporter
|
|
63
64
|
*/
|
|
64
|
-
function createFilteredSpan(span, domainAllowList,
|
|
65
|
-
const payload = extractSpanPayload(span, domainAllowList,
|
|
65
|
+
function createFilteredSpan(span, domainAllowList, globalCaptureRequestBody, globalCaptureResponseBody, transforms) {
|
|
66
|
+
const payload = extractSpanPayload(span, domainAllowList, globalCaptureRequestBody, globalCaptureResponseBody, transforms);
|
|
66
67
|
const filteredAttributes = payload?.attributes ?? span.attributes;
|
|
67
|
-
logger$
|
|
68
|
+
logger$3.debug("Payload", { payload });
|
|
68
69
|
return new Proxy(span, { get(target, prop) {
|
|
69
70
|
if (prop === "attributes") return filteredAttributes;
|
|
70
71
|
const value = target[prop];
|
|
@@ -107,31 +108,27 @@ var PingopsSpanProcessor = class {
|
|
|
107
108
|
this.config = {
|
|
108
109
|
debug: config.debug ?? false,
|
|
109
110
|
sdkVersion: config.sdkVersion,
|
|
110
|
-
headersAllowList: config.headersAllowList,
|
|
111
|
-
headersDenyList: config.headersDenyList,
|
|
112
111
|
domainAllowList: config.domainAllowList,
|
|
113
112
|
domainDenyList: config.domainDenyList,
|
|
114
113
|
captureRequestBody: config.captureRequestBody,
|
|
115
114
|
captureResponseBody: config.captureResponseBody,
|
|
116
|
-
|
|
115
|
+
transforms: config.transforms
|
|
117
116
|
};
|
|
118
117
|
setGlobalConfig({
|
|
119
118
|
captureRequestBody: config.captureRequestBody,
|
|
120
119
|
captureResponseBody: config.captureResponseBody,
|
|
121
120
|
domainAllowList: config.domainAllowList,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
exportTraceUrl: this.exporterTraceUrl
|
|
121
|
+
exportTraceUrl: this.exporterTraceUrl,
|
|
122
|
+
llmMonitoring: config.llmMonitoring
|
|
125
123
|
});
|
|
126
|
-
logger$
|
|
124
|
+
logger$3.info("Initialized PingopsSpanProcessor", {
|
|
127
125
|
baseUrl: config.baseUrl,
|
|
128
126
|
exportMode,
|
|
129
127
|
batchSize: config.batchSize,
|
|
130
128
|
batchTimeout: config.batchTimeout,
|
|
131
129
|
hasDomainAllowList: !!config.domainAllowList && config.domainAllowList.length > 0,
|
|
132
130
|
hasDomainDenyList: !!config.domainDenyList && config.domainDenyList.length > 0,
|
|
133
|
-
|
|
134
|
-
hasHeadersDenyList: !!config.headersDenyList && config.headersDenyList.length > 0
|
|
131
|
+
hasTransforms: !!config.transforms
|
|
135
132
|
});
|
|
136
133
|
}
|
|
137
134
|
/**
|
|
@@ -139,7 +136,7 @@ var PingopsSpanProcessor = class {
|
|
|
139
136
|
*/
|
|
140
137
|
onStart(span, parentContext) {
|
|
141
138
|
const spanContext = span.spanContext();
|
|
142
|
-
logger$
|
|
139
|
+
logger$3.debug("Span started", {
|
|
143
140
|
spanName: span.name,
|
|
144
141
|
spanId: spanContext.spanId,
|
|
145
142
|
traceId: spanContext.traceId
|
|
@@ -148,7 +145,7 @@ var PingopsSpanProcessor = class {
|
|
|
148
145
|
const propagatedAttributes = getPropagatedAttributesFromContext(parentContext);
|
|
149
146
|
if (Object.keys(propagatedAttributes).length > 0) {
|
|
150
147
|
for (const [key, value] of Object.entries(propagatedAttributes)) if (typeof value === "string" || Array.isArray(value)) span.setAttribute(key, value);
|
|
151
|
-
logger$
|
|
148
|
+
logger$3.debug("Set propagated attributes on span", {
|
|
152
149
|
spanName: span.name,
|
|
153
150
|
attributeKeys: Object.keys(propagatedAttributes)
|
|
154
151
|
});
|
|
@@ -166,7 +163,7 @@ var PingopsSpanProcessor = class {
|
|
|
166
163
|
*/
|
|
167
164
|
onEnd(span) {
|
|
168
165
|
const spanContext = span.spanContext();
|
|
169
|
-
logger$
|
|
166
|
+
logger$3.debug("Span ended, processing", {
|
|
170
167
|
spanName: span.name,
|
|
171
168
|
spanId: spanContext.spanId,
|
|
172
169
|
traceId: spanContext.traceId,
|
|
@@ -174,7 +171,7 @@ var PingopsSpanProcessor = class {
|
|
|
174
171
|
});
|
|
175
172
|
try {
|
|
176
173
|
if (!isSpanEligible(span)) {
|
|
177
|
-
logger$
|
|
174
|
+
logger$3.debug("Span not eligible, skipping", {
|
|
178
175
|
spanName: span.name,
|
|
179
176
|
spanId: spanContext.spanId,
|
|
180
177
|
reason: "not CLIENT or missing HTTP/GenAI attributes"
|
|
@@ -184,14 +181,14 @@ var PingopsSpanProcessor = class {
|
|
|
184
181
|
const attributes = span.attributes;
|
|
185
182
|
const url = getHttpUrlFromAttributes(attributes) ?? "";
|
|
186
183
|
if (url && isExporterRequestUrl$1(url, this.exporterTraceUrl)) {
|
|
187
|
-
logger$
|
|
184
|
+
logger$3.debug("Skipping exporter span to prevent self-instrumentation", {
|
|
188
185
|
spanName: span.name,
|
|
189
186
|
spanId: spanContext.spanId,
|
|
190
187
|
url
|
|
191
188
|
});
|
|
192
189
|
return;
|
|
193
190
|
}
|
|
194
|
-
logger$
|
|
191
|
+
logger$3.debug("Extracted URL for domain filtering", {
|
|
195
192
|
spanName: span.name,
|
|
196
193
|
url,
|
|
197
194
|
hasHttpUrl: !!attributes["http.url"],
|
|
@@ -200,25 +197,25 @@ var PingopsSpanProcessor = class {
|
|
|
200
197
|
});
|
|
201
198
|
if (url) {
|
|
202
199
|
if (!shouldCaptureSpan(url, this.config.domainAllowList, this.config.domainDenyList)) {
|
|
203
|
-
logger$
|
|
200
|
+
logger$3.info("Span filtered out by domain rules", {
|
|
204
201
|
spanName: span.name,
|
|
205
202
|
spanId: spanContext.spanId,
|
|
206
203
|
url
|
|
207
204
|
});
|
|
208
205
|
return;
|
|
209
206
|
}
|
|
210
|
-
} else logger$
|
|
211
|
-
const filteredSpan = createFilteredSpan(span, this.config.domainAllowList, this.config.
|
|
207
|
+
} else logger$3.debug("No URL found for domain filtering, proceeding", { spanName: span.name });
|
|
208
|
+
const filteredSpan = createFilteredSpan(span, this.config.domainAllowList, this.config.captureRequestBody, this.config.captureResponseBody, this.config.transforms);
|
|
212
209
|
this.processor.onEnd(filteredSpan);
|
|
213
|
-
logger$
|
|
210
|
+
logger$3.info("Span passed all filters and queued for export", {
|
|
214
211
|
spanName: span.name,
|
|
215
212
|
spanId: spanContext.spanId,
|
|
216
213
|
traceId: spanContext.traceId,
|
|
217
214
|
url,
|
|
218
|
-
|
|
215
|
+
hasTransforms: !!this.config.transforms
|
|
219
216
|
});
|
|
220
217
|
} catch (error) {
|
|
221
|
-
logger$
|
|
218
|
+
logger$3.error("Error processing span", {
|
|
222
219
|
spanName: span.name,
|
|
223
220
|
spanId: spanContext.spanId,
|
|
224
221
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -231,12 +228,12 @@ var PingopsSpanProcessor = class {
|
|
|
231
228
|
* @returns Promise that resolves when all pending operations are complete
|
|
232
229
|
*/
|
|
233
230
|
async forceFlush() {
|
|
234
|
-
logger$
|
|
231
|
+
logger$3.info("Force flushing spans");
|
|
235
232
|
try {
|
|
236
233
|
await this.processor.forceFlush();
|
|
237
|
-
logger$
|
|
234
|
+
logger$3.info("Force flush complete");
|
|
238
235
|
} catch (error) {
|
|
239
|
-
logger$
|
|
236
|
+
logger$3.error("Error during force flush", { error: error instanceof Error ? error.message : String(error) });
|
|
240
237
|
throw error;
|
|
241
238
|
}
|
|
242
239
|
}
|
|
@@ -246,12 +243,12 @@ var PingopsSpanProcessor = class {
|
|
|
246
243
|
* @returns Promise that resolves when shutdown is complete
|
|
247
244
|
*/
|
|
248
245
|
async shutdown() {
|
|
249
|
-
logger$
|
|
246
|
+
logger$3.info("Shutting down processor");
|
|
250
247
|
try {
|
|
251
248
|
await this.processor.shutdown();
|
|
252
|
-
logger$
|
|
249
|
+
logger$3.info("Processor shutdown complete");
|
|
253
250
|
} catch (error) {
|
|
254
|
-
logger$
|
|
251
|
+
logger$3.error("Error during processor shutdown", { error: error instanceof Error ? error.message : String(error) });
|
|
255
252
|
throw error;
|
|
256
253
|
}
|
|
257
254
|
}
|
|
@@ -266,7 +263,7 @@ const PINGOPS_GLOBAL_SYMBOL = Symbol.for("pingops");
|
|
|
266
263
|
/**
|
|
267
264
|
* Logger instance for tracer provider
|
|
268
265
|
*/
|
|
269
|
-
const logger$
|
|
266
|
+
const logger$2 = createLogger("[PingOps TracerProvider]");
|
|
270
267
|
/**
|
|
271
268
|
* Creates initial global state
|
|
272
269
|
*/
|
|
@@ -281,21 +278,21 @@ function getGlobalState() {
|
|
|
281
278
|
try {
|
|
282
279
|
const g = globalThis;
|
|
283
280
|
if (typeof g !== "object" || g === null) {
|
|
284
|
-
logger$
|
|
281
|
+
logger$2.warn("globalThis is not available, using fallback state");
|
|
285
282
|
return initialState;
|
|
286
283
|
}
|
|
287
284
|
if (!g[PINGOPS_GLOBAL_SYMBOL]) {
|
|
288
|
-
logger$
|
|
285
|
+
logger$2.debug("Creating new global state");
|
|
289
286
|
Object.defineProperty(g, PINGOPS_GLOBAL_SYMBOL, {
|
|
290
287
|
value: initialState,
|
|
291
288
|
writable: false,
|
|
292
289
|
configurable: false,
|
|
293
290
|
enumerable: false
|
|
294
291
|
});
|
|
295
|
-
} else logger$
|
|
292
|
+
} else logger$2.debug("Retrieved existing global state");
|
|
296
293
|
return g[PINGOPS_GLOBAL_SYMBOL];
|
|
297
294
|
} catch (err) {
|
|
298
|
-
logger$
|
|
295
|
+
logger$2.error("Failed to access global state:", err instanceof Error ? err.message : String(err));
|
|
299
296
|
return initialState;
|
|
300
297
|
}
|
|
301
298
|
}
|
|
@@ -313,11 +310,11 @@ function setPingopsTracerProvider(provider) {
|
|
|
313
310
|
const state = getGlobalState();
|
|
314
311
|
const hadProvider = state.isolatedTracerProvider !== null;
|
|
315
312
|
state.isolatedTracerProvider = provider;
|
|
316
|
-
if (provider) logger$
|
|
313
|
+
if (provider) logger$2.info("Set isolated TracerProvider", {
|
|
317
314
|
hadPrevious: hadProvider,
|
|
318
315
|
providerType: provider.constructor.name
|
|
319
316
|
});
|
|
320
|
-
else logger$
|
|
317
|
+
else logger$2.info("Cleared isolated TracerProvider", { hadPrevious: hadProvider });
|
|
321
318
|
}
|
|
322
319
|
/**
|
|
323
320
|
* Gets the TracerProvider for PingOps tracing operations.
|
|
@@ -331,36 +328,36 @@ function setPingopsTracerProvider(provider) {
|
|
|
331
328
|
function getPingopsTracerProvider() {
|
|
332
329
|
const { isolatedTracerProvider } = getGlobalState();
|
|
333
330
|
if (isolatedTracerProvider) {
|
|
334
|
-
logger$
|
|
331
|
+
logger$2.debug("Using isolated TracerProvider", { providerType: isolatedTracerProvider.constructor.name });
|
|
335
332
|
return isolatedTracerProvider;
|
|
336
333
|
}
|
|
337
334
|
const globalProvider = trace.getTracerProvider();
|
|
338
|
-
logger$
|
|
335
|
+
logger$2.debug("Using global TracerProvider", { providerType: globalProvider.constructor.name });
|
|
339
336
|
return globalProvider;
|
|
340
337
|
}
|
|
341
338
|
/**
|
|
342
339
|
* Shuts down the TracerProvider and flushes remaining spans
|
|
343
340
|
*/
|
|
344
341
|
async function shutdownTracerProvider() {
|
|
345
|
-
logger$
|
|
342
|
+
logger$2.info("Shutting down TracerProvider");
|
|
346
343
|
const providerWithShutdown = getPingopsTracerProvider();
|
|
347
344
|
if (providerWithShutdown && typeof providerWithShutdown.shutdown === "function") {
|
|
348
|
-
logger$
|
|
345
|
+
logger$2.debug("Calling provider.shutdown()");
|
|
349
346
|
try {
|
|
350
347
|
await providerWithShutdown.shutdown();
|
|
351
|
-
logger$
|
|
348
|
+
logger$2.info("TracerProvider shutdown complete");
|
|
352
349
|
} catch (error) {
|
|
353
|
-
logger$
|
|
350
|
+
logger$2.error("Error during TracerProvider shutdown:", error instanceof Error ? error.message : String(error));
|
|
354
351
|
throw error;
|
|
355
352
|
}
|
|
356
|
-
} else logger$
|
|
353
|
+
} else logger$2.warn("TracerProvider does not have shutdown method, skipping");
|
|
357
354
|
setPingopsTracerProvider(null);
|
|
358
|
-
logger$
|
|
355
|
+
logger$2.info("TracerProvider shutdown finished");
|
|
359
356
|
}
|
|
360
357
|
|
|
361
358
|
//#endregion
|
|
362
359
|
//#region src/instrumentations/suppression-guard.ts
|
|
363
|
-
const logger = createLogger("[PingOps SuppressionGuard]");
|
|
360
|
+
const logger$1 = createLogger("[PingOps SuppressionGuard]");
|
|
364
361
|
let hasLoggedSuppressionLeakWarning = false;
|
|
365
362
|
function normalizePath(pathname) {
|
|
366
363
|
return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
|
|
@@ -400,9 +397,9 @@ function resolveOutboundSpanParentContext(activeContext, requestUrl) {
|
|
|
400
397
|
if (activeContext.getValue(PINGOPS_INTENTIONAL_SUPPRESSION) === true) return activeContext;
|
|
401
398
|
if (isExporterRequestUrl(requestUrl)) return activeContext;
|
|
402
399
|
if (!hasLoggedSuppressionLeakWarning) {
|
|
403
|
-
logger.warn("Detected suppressed context for outbound user request; running instrumentation on ROOT_CONTEXT to prevent Noop spans from suppression leakage");
|
|
400
|
+
logger$1.warn("Detected suppressed context for outbound user request; running instrumentation on ROOT_CONTEXT to prevent Noop spans from suppression leakage");
|
|
404
401
|
hasLoggedSuppressionLeakWarning = true;
|
|
405
|
-
} else logger.debug("Suppressed context detected for outbound user request; using ROOT_CONTEXT");
|
|
402
|
+
} else logger$1.debug("Suppressed context detected for outbound user request; using ROOT_CONTEXT");
|
|
406
403
|
return ROOT_CONTEXT;
|
|
407
404
|
}
|
|
408
405
|
|
|
@@ -412,8 +409,32 @@ const HTTP_REQUEST_BODY = "http.request.body";
|
|
|
412
409
|
const HTTP_RESPONSE_BODY = "http.response.body";
|
|
413
410
|
const HTTP_REQUEST_BODY_SIZE = "http.request.body.size";
|
|
414
411
|
const HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";
|
|
415
|
-
const
|
|
416
|
-
const
|
|
412
|
+
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
413
|
+
const BINARY_CONTENT_TYPES = new Set([
|
|
414
|
+
"application/octet-stream",
|
|
415
|
+
"application/pdf",
|
|
416
|
+
"application/zip",
|
|
417
|
+
"application/gzip",
|
|
418
|
+
"application/x-gzip",
|
|
419
|
+
"image/jpeg",
|
|
420
|
+
"image/jpg",
|
|
421
|
+
"image/png",
|
|
422
|
+
"image/gif",
|
|
423
|
+
"image/webp",
|
|
424
|
+
"image/bmp",
|
|
425
|
+
"image/tiff",
|
|
426
|
+
"image/ico",
|
|
427
|
+
"audio/mpeg",
|
|
428
|
+
"audio/mp3",
|
|
429
|
+
"audio/wav",
|
|
430
|
+
"audio/ogg",
|
|
431
|
+
"audio/webm",
|
|
432
|
+
"video/mp4",
|
|
433
|
+
"video/webm",
|
|
434
|
+
"video/ogg",
|
|
435
|
+
"video/avi",
|
|
436
|
+
"video/mov"
|
|
437
|
+
]);
|
|
417
438
|
/**
|
|
418
439
|
* Gets domain rule configuration for a given URL.
|
|
419
440
|
*/
|
|
@@ -461,6 +482,395 @@ function toBufferChunk(data) {
|
|
|
461
482
|
if (data instanceof Uint8Array) return Buffer.from(data);
|
|
462
483
|
return null;
|
|
463
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Parses a content-length value into a positive byte count.
|
|
487
|
+
*/
|
|
488
|
+
function parseContentLength(value) {
|
|
489
|
+
if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
490
|
+
if (typeof value === "string") {
|
|
491
|
+
const parsed = Number(value);
|
|
492
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
|
|
493
|
+
}
|
|
494
|
+
if (Array.isArray(value) && value.length > 0) return parseContentLength(value[0]);
|
|
495
|
+
}
|
|
496
|
+
function normalizeHeaderValue$1(v) {
|
|
497
|
+
if (typeof v === "string") {
|
|
498
|
+
const trimmed = v.trim();
|
|
499
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
500
|
+
}
|
|
501
|
+
if (Array.isArray(v) && v.length > 0) return normalizeHeaderValue$1(v[0]);
|
|
502
|
+
if (typeof v === "number" && Number.isFinite(v)) return String(v);
|
|
503
|
+
}
|
|
504
|
+
function parseContentTypeMainType(contentType) {
|
|
505
|
+
const normalized = normalizeHeaderValue$1(contentType);
|
|
506
|
+
if (!normalized) return;
|
|
507
|
+
return normalized.toLowerCase().split(";")[0]?.trim() || void 0;
|
|
508
|
+
}
|
|
509
|
+
function isUtf8(buffer) {
|
|
510
|
+
if (buffer.length === 0) return true;
|
|
511
|
+
try {
|
|
512
|
+
UTF8_DECODER.decode(buffer);
|
|
513
|
+
return true;
|
|
514
|
+
} catch {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Encodes HTTP body for span attributes:
|
|
520
|
+
* - compressed payloads => base64 (backend can decode)
|
|
521
|
+
* - known binary content-types => base64
|
|
522
|
+
* - known textual/utf8 payloads => utf8
|
|
523
|
+
*/
|
|
524
|
+
function encodeBodyBufferForSpan(buffer, headers) {
|
|
525
|
+
if (!buffer || buffer.length === 0) return null;
|
|
526
|
+
const contentEncoding = normalizeHeaderValue$1(headers?.["content-encoding"]);
|
|
527
|
+
if (isCompressedContentEncoding(contentEncoding)) return {
|
|
528
|
+
content: buffer.toString("base64"),
|
|
529
|
+
contentEncoding: contentEncoding?.split(",")[0]?.trim().toLowerCase() || void 0
|
|
530
|
+
};
|
|
531
|
+
const contentType = parseContentTypeMainType(headers?.["content-type"]);
|
|
532
|
+
if (contentType && BINARY_CONTENT_TYPES.has(contentType)) return { content: buffer.toString("base64") };
|
|
533
|
+
if (isUtf8(buffer)) return { content: buffer.toString("utf8") };
|
|
534
|
+
return { content: buffer.toString("base64") };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/llm/types.ts
|
|
539
|
+
const GEN_AI_ATTRS = {
|
|
540
|
+
SYSTEM: "gen_ai.system",
|
|
541
|
+
PROVIDER_NAME: "gen_ai.provider.name",
|
|
542
|
+
OPERATION_NAME: "gen_ai.operation.name",
|
|
543
|
+
REQUEST_MODEL: "gen_ai.request.model",
|
|
544
|
+
RESPONSE_MODEL: "gen_ai.response.model",
|
|
545
|
+
RESPONSE_ID: "gen_ai.response.id",
|
|
546
|
+
USAGE_INPUT_TOKENS: "gen_ai.usage.input_tokens",
|
|
547
|
+
USAGE_OUTPUT_TOKENS: "gen_ai.usage.output_tokens"
|
|
548
|
+
};
|
|
549
|
+
const PINGOPS_GEN_AI_ATTRS = {
|
|
550
|
+
TOTAL_TOKENS: "pingops.gen_ai.usage.total_tokens",
|
|
551
|
+
CACHE_READ_INPUT_TOKENS: "pingops.gen_ai.usage.cache_read_input_tokens",
|
|
552
|
+
CACHE_CREATION_INPUT_TOKENS: "pingops.gen_ai.usage.cache_creation_input_tokens",
|
|
553
|
+
CACHE_TOKENS: "pingops.gen_ai.usage.cache_tokens"
|
|
554
|
+
};
|
|
555
|
+
const DEFAULT_LLM_MONITORING_CONFIG = {
|
|
556
|
+
enabled: false,
|
|
557
|
+
streaming: true
|
|
558
|
+
};
|
|
559
|
+
function normalizeLlmMonitoringConfig(config) {
|
|
560
|
+
return {
|
|
561
|
+
enabled: config?.enabled ?? DEFAULT_LLM_MONITORING_CONFIG.enabled,
|
|
562
|
+
streaming: config?.streaming ?? DEFAULT_LLM_MONITORING_CONFIG.streaming
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
//#endregion
|
|
567
|
+
//#region src/llm/provider-detector.ts
|
|
568
|
+
function hostMatches(host, suffixes) {
|
|
569
|
+
return suffixes.some((suffix) => host === suffix || host.endsWith(`.${suffix}`));
|
|
570
|
+
}
|
|
571
|
+
function includesAny(pathname, values) {
|
|
572
|
+
return values.some((value) => pathname.includes(value));
|
|
573
|
+
}
|
|
574
|
+
function detectLlmProvider(url, llmConfig) {
|
|
575
|
+
if (!url) return;
|
|
576
|
+
if (!normalizeLlmMonitoringConfig(llmConfig).enabled) return;
|
|
577
|
+
try {
|
|
578
|
+
const parsed = new URL(url);
|
|
579
|
+
const host = parsed.hostname.toLowerCase();
|
|
580
|
+
const path = parsed.pathname.toLowerCase();
|
|
581
|
+
const maybeProvider = [];
|
|
582
|
+
if (hostMatches(host, ["x.ai", "api.x.ai"])) maybeProvider.push({
|
|
583
|
+
provider: "xai",
|
|
584
|
+
providerName: "xai"
|
|
585
|
+
});
|
|
586
|
+
if (hostMatches(host, ["openai.com", "api.openai.com"]) || includesAny(path, [
|
|
587
|
+
"/v1/chat/completions",
|
|
588
|
+
"/v1/responses",
|
|
589
|
+
"/v1/completions"
|
|
590
|
+
]) && !host.includes("x.ai")) maybeProvider.push({
|
|
591
|
+
provider: "openai",
|
|
592
|
+
providerName: "openai"
|
|
593
|
+
});
|
|
594
|
+
if (hostMatches(host, ["anthropic.com", "api.anthropic.com"]) || includesAny(path, ["/v1/messages", "/v1/complete"]) && host.includes("anthropic")) maybeProvider.push({
|
|
595
|
+
provider: "anthropic",
|
|
596
|
+
providerName: "anthropic"
|
|
597
|
+
});
|
|
598
|
+
if (hostMatches(host, ["googleapis.com", "generativelanguage.googleapis.com"]) || includesAny(path, [
|
|
599
|
+
":generatecontent",
|
|
600
|
+
":streamgeneratecontent",
|
|
601
|
+
"/models/"
|
|
602
|
+
])) {
|
|
603
|
+
if (host.includes("google") || path.includes("generatecontent")) maybeProvider.push({
|
|
604
|
+
provider: "gemini",
|
|
605
|
+
providerName: "gemini"
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
return maybeProvider[0];
|
|
609
|
+
} catch {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function deriveOperationName(url) {
|
|
614
|
+
if (!url) return;
|
|
615
|
+
try {
|
|
616
|
+
const pathname = new URL(url).pathname.toLowerCase();
|
|
617
|
+
if (pathname.includes("/embeddings")) return "embeddings";
|
|
618
|
+
if (pathname.includes("/responses")) return "responses";
|
|
619
|
+
if (pathname.includes("/chat/completions") || pathname.includes("/messages")) return "chat.completions";
|
|
620
|
+
if (pathname.includes("/completions") || pathname.includes("/complete")) return "completions";
|
|
621
|
+
if (pathname.includes("generatecontent")) return "chat.completions";
|
|
622
|
+
return;
|
|
623
|
+
} catch {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/llm/request-parser.ts
|
|
630
|
+
function tryParseJson(raw) {
|
|
631
|
+
if (!raw || raw.length === 0) return;
|
|
632
|
+
try {
|
|
633
|
+
const parsed = JSON.parse(raw);
|
|
634
|
+
if (typeof parsed === "object" && parsed !== null) return parsed;
|
|
635
|
+
return;
|
|
636
|
+
} catch {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function getStringField(obj, key) {
|
|
641
|
+
if (!obj) return;
|
|
642
|
+
const value = obj[key];
|
|
643
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
644
|
+
}
|
|
645
|
+
function parseLlmRequestData(provider, requestBody, url) {
|
|
646
|
+
const parsed = tryParseJson(requestBody);
|
|
647
|
+
const data = {};
|
|
648
|
+
if (provider === "openai" || provider === "xai") data.model = getStringField(parsed, "model") || getStringField(parsed, "response_model") || void 0;
|
|
649
|
+
else if (provider === "anthropic") data.model = getStringField(parsed, "model") || void 0;
|
|
650
|
+
else if (provider === "gemini") data.model = getStringField(parsed, "model") || getModelFromGeminiPath(url);
|
|
651
|
+
return data;
|
|
652
|
+
}
|
|
653
|
+
function getModelFromGeminiPath(url) {
|
|
654
|
+
if (!url) return;
|
|
655
|
+
try {
|
|
656
|
+
const pathname = new URL(url).pathname;
|
|
657
|
+
const markerIndex = pathname.indexOf("/models/");
|
|
658
|
+
if (markerIndex < 0) return;
|
|
659
|
+
const modelPart = pathname.slice(markerIndex + 8);
|
|
660
|
+
const endIndex = modelPart.indexOf(":");
|
|
661
|
+
const candidate = endIndex >= 0 ? modelPart.slice(0, endIndex) : modelPart;
|
|
662
|
+
return candidate.length > 0 ? candidate : void 0;
|
|
663
|
+
} catch {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/llm/response-parser.ts
|
|
670
|
+
function toNonNegativeNumber(value) {
|
|
671
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 0) return;
|
|
672
|
+
return value;
|
|
673
|
+
}
|
|
674
|
+
function parseOpenAiLikeUsage(response) {
|
|
675
|
+
const usage = typeof response.usage === "object" && response.usage !== null ? response.usage : void 0;
|
|
676
|
+
const promptDetails = usage && typeof usage.prompt_tokens_details === "object" && usage.prompt_tokens_details !== null ? usage.prompt_tokens_details : void 0;
|
|
677
|
+
return {
|
|
678
|
+
responseModel: typeof response.model === "string" ? response.model : void 0,
|
|
679
|
+
responseId: typeof response.id === "string" ? response.id : void 0,
|
|
680
|
+
inputTokens: usage ? toNonNegativeNumber(usage.prompt_tokens) : void 0,
|
|
681
|
+
outputTokens: usage ? toNonNegativeNumber(usage.completion_tokens) : void 0,
|
|
682
|
+
totalTokens: usage ? toNonNegativeNumber(usage.total_tokens) : void 0,
|
|
683
|
+
cacheReadInputTokens: promptDetails ? toNonNegativeNumber(promptDetails.cached_tokens) : void 0
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function parseAnthropicUsage(response) {
|
|
687
|
+
const message = typeof response.message === "object" && response.message !== null ? response.message : void 0;
|
|
688
|
+
const usage = typeof response.usage === "object" && response.usage !== null ? response.usage : message && typeof message.usage === "object" && message.usage !== null ? message.usage : void 0;
|
|
689
|
+
return {
|
|
690
|
+
responseModel: typeof response.model === "string" ? response.model : typeof message?.model === "string" ? message.model : void 0,
|
|
691
|
+
responseId: typeof response.id === "string" ? response.id : typeof message?.id === "string" ? message.id : void 0,
|
|
692
|
+
inputTokens: usage ? toNonNegativeNumber(usage.input_tokens) : void 0,
|
|
693
|
+
outputTokens: usage ? toNonNegativeNumber(usage.output_tokens) : void 0,
|
|
694
|
+
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,
|
|
695
|
+
cacheReadInputTokens: usage ? toNonNegativeNumber(usage.cache_read_input_tokens) : void 0,
|
|
696
|
+
cacheCreationInputTokens: usage ? toNonNegativeNumber(usage.cache_creation_input_tokens) : void 0
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function parseGeminiUsage(response) {
|
|
700
|
+
const usage = typeof response.usageMetadata === "object" && response.usageMetadata !== null ? response.usageMetadata : void 0;
|
|
701
|
+
let responseModel;
|
|
702
|
+
if (typeof response.modelVersion === "string") responseModel = response.modelVersion;
|
|
703
|
+
else if (Array.isArray(response.candidates) && response.candidates.length > 0) {
|
|
704
|
+
const first = response.candidates[0];
|
|
705
|
+
if (typeof first === "object" && first !== null && "model" in first) responseModel = typeof first.model === "string" ? first.model : void 0;
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
responseModel,
|
|
709
|
+
responseId: typeof response.responseId === "string" ? response.responseId : void 0,
|
|
710
|
+
inputTokens: usage ? toNonNegativeNumber(usage.promptTokenCount) : void 0,
|
|
711
|
+
outputTokens: usage ? toNonNegativeNumber(usage.candidatesTokenCount) : void 0,
|
|
712
|
+
totalTokens: usage ? toNonNegativeNumber(usage.totalTokenCount) : void 0,
|
|
713
|
+
cacheReadInputTokens: usage ? toNonNegativeNumber(usage.cachedContentTokenCount) : void 0
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
function parseJsonObject(raw) {
|
|
717
|
+
if (!raw || raw.length === 0) return;
|
|
718
|
+
try {
|
|
719
|
+
const value = JSON.parse(raw);
|
|
720
|
+
if (typeof value === "object" && value !== null) return value;
|
|
721
|
+
return;
|
|
722
|
+
} catch {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function mergeUsage(target, next) {
|
|
727
|
+
return {
|
|
728
|
+
responseModel: next.responseModel ?? target.responseModel,
|
|
729
|
+
responseId: next.responseId ?? target.responseId,
|
|
730
|
+
inputTokens: next.inputTokens ?? target.inputTokens,
|
|
731
|
+
outputTokens: next.outputTokens ?? target.outputTokens,
|
|
732
|
+
totalTokens: next.totalTokens ?? target.totalTokens,
|
|
733
|
+
cacheReadInputTokens: next.cacheReadInputTokens ?? target.cacheReadInputTokens,
|
|
734
|
+
cacheCreationInputTokens: next.cacheCreationInputTokens ?? target.cacheCreationInputTokens
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function parseByProvider(provider, object) {
|
|
738
|
+
if (provider === "openai" || provider === "xai") return parseOpenAiLikeUsage(object);
|
|
739
|
+
if (provider === "anthropic") return parseAnthropicUsage(object);
|
|
740
|
+
return parseGeminiUsage(object);
|
|
741
|
+
}
|
|
742
|
+
function parseSsePayload(provider, raw) {
|
|
743
|
+
if (!raw || raw.length === 0) return {};
|
|
744
|
+
let acc = {};
|
|
745
|
+
const lines = raw.split(/\r?\n/);
|
|
746
|
+
for (const line of lines) {
|
|
747
|
+
const trimmed = line.trim();
|
|
748
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
749
|
+
const payload = trimmed.slice(5).trim();
|
|
750
|
+
if (payload.length === 0 || payload === "[DONE]") continue;
|
|
751
|
+
const parsed = parseJsonObject(payload);
|
|
752
|
+
if (!parsed) continue;
|
|
753
|
+
acc = mergeUsage(acc, parseByProvider(provider, parsed));
|
|
754
|
+
}
|
|
755
|
+
return acc;
|
|
756
|
+
}
|
|
757
|
+
function parseLlmResponseData(provider, responseBody, streaming = true) {
|
|
758
|
+
if (!responseBody || responseBody.length === 0) return {};
|
|
759
|
+
const parsedJson = parseJsonObject(responseBody);
|
|
760
|
+
if (parsedJson) return parseByProvider(provider, parsedJson);
|
|
761
|
+
if (!streaming) return {};
|
|
762
|
+
return parseSsePayload(provider, responseBody);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
//#endregion
|
|
766
|
+
//#region src/llm/enricher.ts
|
|
767
|
+
const logger = createLogger("[PingOps LLM Enricher]");
|
|
768
|
+
function getEffectiveConfig() {
|
|
769
|
+
return normalizeLlmMonitoringConfig(getGlobalConfig()?.llmMonitoring);
|
|
770
|
+
}
|
|
771
|
+
function createLlmEnrichmentState(span, url, requestMethod, requestHeaders) {
|
|
772
|
+
const config = getEffectiveConfig();
|
|
773
|
+
if (!config.enabled) return;
|
|
774
|
+
const detection = detectLlmProvider(url, config);
|
|
775
|
+
if (!detection) return;
|
|
776
|
+
return {
|
|
777
|
+
span,
|
|
778
|
+
url,
|
|
779
|
+
requestMethod,
|
|
780
|
+
requestHeaders,
|
|
781
|
+
detection,
|
|
782
|
+
requestData: void 0,
|
|
783
|
+
responseData: void 0,
|
|
784
|
+
responseHeaders: void 0,
|
|
785
|
+
requestParseBytes: 0,
|
|
786
|
+
responseParseBytes: 0,
|
|
787
|
+
requestBodyBuffer: [],
|
|
788
|
+
responseBodyBuffer: [],
|
|
789
|
+
requestParseStopped: false,
|
|
790
|
+
responseParseStopped: false
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function updateLlmRequestHeaders(state, headers) {
|
|
794
|
+
state.requestHeaders = headers;
|
|
795
|
+
}
|
|
796
|
+
function updateLlmResponseHeaders(state, headers) {
|
|
797
|
+
state.responseHeaders = headers;
|
|
798
|
+
}
|
|
799
|
+
function appendLlmRequestChunk(state, chunk) {
|
|
800
|
+
state.requestBodyBuffer.push(chunk);
|
|
801
|
+
state.requestParseBytes += chunk.length;
|
|
802
|
+
}
|
|
803
|
+
function appendLlmResponseChunk(state, chunk) {
|
|
804
|
+
state.responseBodyBuffer.push(chunk);
|
|
805
|
+
state.responseParseBytes += chunk.length;
|
|
806
|
+
}
|
|
807
|
+
function computeTotalTokens(responseData) {
|
|
808
|
+
if (responseData.totalTokens !== void 0) return responseData.totalTokens;
|
|
809
|
+
if (responseData.inputTokens !== void 0 || responseData.outputTokens !== void 0) return (responseData.inputTokens ?? 0) + (responseData.outputTokens ?? 0);
|
|
810
|
+
}
|
|
811
|
+
function normalizeHeaderValue(value) {
|
|
812
|
+
if (typeof value === "string") return value;
|
|
813
|
+
if (Array.isArray(value) && value.length > 0) return value.join(",");
|
|
814
|
+
}
|
|
815
|
+
function decodeResponseBody(body, headers) {
|
|
816
|
+
const encodingValue = normalizeHeaderValue(headers?.["content-encoding"]);
|
|
817
|
+
if (!encodingValue || encodingValue.trim().length === 0) return body.toString("utf8");
|
|
818
|
+
const encodings = encodingValue.toLowerCase().split(",").map((v) => v.trim()).filter(Boolean);
|
|
819
|
+
if (encodings.length === 0 || encodings.includes("identity")) return body.toString("utf8");
|
|
820
|
+
let decoded = body;
|
|
821
|
+
for (let i = encodings.length - 1; i >= 0; i -= 1) {
|
|
822
|
+
const encoding = encodings[i];
|
|
823
|
+
if (encoding === "gzip" || encoding === "x-gzip") {
|
|
824
|
+
decoded = gunzipSync(decoded);
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (encoding === "deflate") {
|
|
828
|
+
decoded = inflateSync(decoded);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (encoding === "br") {
|
|
832
|
+
decoded = brotliDecompressSync(decoded);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
return body.toString("utf8");
|
|
836
|
+
}
|
|
837
|
+
return decoded.toString("utf8");
|
|
838
|
+
}
|
|
839
|
+
function finalizeLlmEnrichment(state) {
|
|
840
|
+
const config = getEffectiveConfig();
|
|
841
|
+
if (!config.enabled || !state.detection) return;
|
|
842
|
+
try {
|
|
843
|
+
const requestBody = state.requestBodyBuffer.length > 0 ? Buffer.concat(state.requestBodyBuffer).toString("utf8") : void 0;
|
|
844
|
+
if (state.requestData === void 0) state.requestData = parseLlmRequestData(state.detection.provider, requestBody, state.url);
|
|
845
|
+
const operationName = deriveOperationName(state.url);
|
|
846
|
+
const responseBody = state.responseBodyBuffer.length > 0 ? decodeResponseBody(Buffer.concat(state.responseBodyBuffer), state.responseHeaders) : void 0;
|
|
847
|
+
const parsedResponse = responseBody ? parseLlmResponseData(state.detection.provider, responseBody, config.streaming) : {};
|
|
848
|
+
state.responseData = {
|
|
849
|
+
...state.responseData,
|
|
850
|
+
...parsedResponse
|
|
851
|
+
};
|
|
852
|
+
state.span.setAttribute(GEN_AI_ATTRS.SYSTEM, state.detection.provider);
|
|
853
|
+
state.span.setAttribute(GEN_AI_ATTRS.PROVIDER_NAME, state.detection.providerName);
|
|
854
|
+
if (operationName) state.span.setAttribute(GEN_AI_ATTRS.OPERATION_NAME, operationName);
|
|
855
|
+
if (state.requestData?.model) state.span.setAttribute(GEN_AI_ATTRS.REQUEST_MODEL, state.requestData.model);
|
|
856
|
+
if (state.responseData?.responseModel) state.span.setAttribute(GEN_AI_ATTRS.RESPONSE_MODEL, state.responseData.responseModel);
|
|
857
|
+
if (state.responseData?.responseId) state.span.setAttribute(GEN_AI_ATTRS.RESPONSE_ID, state.responseData.responseId);
|
|
858
|
+
if (state.responseData?.inputTokens !== void 0) state.span.setAttribute(GEN_AI_ATTRS.USAGE_INPUT_TOKENS, state.responseData.inputTokens);
|
|
859
|
+
if (state.responseData?.outputTokens !== void 0) state.span.setAttribute(GEN_AI_ATTRS.USAGE_OUTPUT_TOKENS, state.responseData.outputTokens);
|
|
860
|
+
const totalTokens = state.responseData ? computeTotalTokens(state.responseData) : void 0;
|
|
861
|
+
if (totalTokens !== void 0) state.span.setAttribute(PINGOPS_GEN_AI_ATTRS.TOTAL_TOKENS, totalTokens);
|
|
862
|
+
if (state.responseData?.cacheReadInputTokens !== void 0) state.span.setAttribute(PINGOPS_GEN_AI_ATTRS.CACHE_READ_INPUT_TOKENS, state.responseData.cacheReadInputTokens);
|
|
863
|
+
if (state.responseData?.cacheCreationInputTokens !== void 0) state.span.setAttribute(PINGOPS_GEN_AI_ATTRS.CACHE_CREATION_INPUT_TOKENS, state.responseData.cacheCreationInputTokens);
|
|
864
|
+
const cacheTokens = (state.responseData?.cacheReadInputTokens ?? 0) + (state.responseData?.cacheCreationInputTokens ?? 0);
|
|
865
|
+
if (cacheTokens > 0) state.span.setAttribute(PINGOPS_GEN_AI_ATTRS.CACHE_TOKENS, cacheTokens);
|
|
866
|
+
} catch (error) {
|
|
867
|
+
logger.debug("Failed to enrich LLM attributes", {
|
|
868
|
+
error: error instanceof Error ? error.message : String(error),
|
|
869
|
+
url: state.url,
|
|
870
|
+
provider: state.detection.provider
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
464
874
|
|
|
465
875
|
//#endregion
|
|
466
876
|
//#region src/instrumentations/http/pingops-http.ts
|
|
@@ -507,9 +917,9 @@ function setAttributeValue(span, attrName, attrValue) {
|
|
|
507
917
|
/**
|
|
508
918
|
* Captures request body from a chunk buffer.
|
|
509
919
|
*/
|
|
510
|
-
function captureRequestBody(span, data,
|
|
920
|
+
function captureRequestBody(span, data, semanticAttr, url) {
|
|
511
921
|
if (!shouldCaptureRequestBody(url)) return;
|
|
512
|
-
if (data.length
|
|
922
|
+
if (data.length) try {
|
|
513
923
|
const requestBody = data.toString("utf-8");
|
|
514
924
|
if (requestBody) setAttributeValue(span, semanticAttr, requestBody);
|
|
515
925
|
} catch (e) {
|
|
@@ -519,25 +929,13 @@ function captureRequestBody(span, data, maxSize, semanticAttr, url) {
|
|
|
519
929
|
/**
|
|
520
930
|
* Captures response body from chunks
|
|
521
931
|
*/
|
|
522
|
-
function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url
|
|
932
|
+
function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url) {
|
|
523
933
|
if (!shouldCaptureResponseBody(url)) return;
|
|
524
|
-
if (chunks === null) {
|
|
525
|
-
const contentEncoding = responseHeaders?.["content-encoding"];
|
|
526
|
-
const contentType = responseHeaders?.["content-type"];
|
|
527
|
-
const toHeaderString = (value) => typeof value === "string" ? value : Array.isArray(value) ? value.join(", ") : "unknown";
|
|
528
|
-
setAttributeValue(span, semanticAttr, `[truncated response body; exceeded maxResponseBodySize=${maxSize ?? DEFAULT_MAX_RESPONSE_BODY_SIZE}; content-type=${toHeaderString(contentType)}; content-encoding=${toHeaderString(contentEncoding)}]`);
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
934
|
if (chunks.length) try {
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
setAttributeValue(span,
|
|
536
|
-
const encStr = typeof contentEncoding === "string" ? contentEncoding : Array.isArray(contentEncoding) ? contentEncoding.map(String).join(", ") : void 0;
|
|
537
|
-
if (encStr) setAttributeValue(span, HTTP_RESPONSE_CONTENT_ENCODING, encStr);
|
|
538
|
-
} else {
|
|
539
|
-
const bodyStr = bufferToBodyString(concatedChunks);
|
|
540
|
-
if (bodyStr != null) setAttributeValue(span, semanticAttr, bodyStr);
|
|
935
|
+
const encoded = encodeBodyBufferForSpan(Buffer.concat(chunks), responseHeaders);
|
|
936
|
+
if (encoded) {
|
|
937
|
+
setAttributeValue(span, semanticAttr, encoded.content);
|
|
938
|
+
if (encoded.contentEncoding) setAttributeValue(span, HTTP_RESPONSE_CONTENT_ENCODING, encoded.contentEncoding);
|
|
541
939
|
}
|
|
542
940
|
} catch (e) {
|
|
543
941
|
console.error("Error occurred while capturing response body:", e);
|
|
@@ -614,6 +1012,7 @@ function extractClientRequestPath(request) {
|
|
|
614
1012
|
}
|
|
615
1013
|
const PingopsHttpSemanticAttributes = PingopsSemanticAttributes;
|
|
616
1014
|
var PingopsHttpInstrumentation = class extends HttpInstrumentation {
|
|
1015
|
+
_llmStateByRequest = /* @__PURE__ */ new WeakMap();
|
|
617
1016
|
constructor(config) {
|
|
618
1017
|
super(config);
|
|
619
1018
|
this._config = this._createConfig(config);
|
|
@@ -635,21 +1034,26 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
|
|
|
635
1034
|
_createConfig(config) {
|
|
636
1035
|
return {
|
|
637
1036
|
...config,
|
|
638
|
-
requestHook: this._createRequestHook(config?.requestHook
|
|
639
|
-
responseHook: this._createResponseHook(config?.responseHook
|
|
1037
|
+
requestHook: this._createRequestHook(config?.requestHook),
|
|
1038
|
+
responseHook: this._createResponseHook(config?.responseHook)
|
|
640
1039
|
};
|
|
641
1040
|
}
|
|
642
|
-
_createRequestHook(originalRequestHook
|
|
1041
|
+
_createRequestHook(originalRequestHook) {
|
|
643
1042
|
return (span, request) => {
|
|
644
1043
|
const headers = extractRequestHeaders(request);
|
|
645
1044
|
if (headers) captureRequestHeaders(span, headers);
|
|
646
1045
|
if (request instanceof ClientRequest) {
|
|
647
|
-
const maxRequestBodySize = config?.maxRequestBodySize || DEFAULT_MAX_REQUEST_BODY_SIZE;
|
|
648
1046
|
let requestBodySize = 0;
|
|
649
|
-
|
|
1047
|
+
const requestContentLength = parseContentLength(headers?.["content-length"]);
|
|
1048
|
+
span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestContentLength !== void 0 ? Math.max(requestBodySize, requestContentLength) : requestBodySize);
|
|
650
1049
|
const hostHeader = request.getHeader("host");
|
|
651
1050
|
const host = typeof hostHeader === "string" ? hostHeader : Array.isArray(hostHeader) ? hostHeader.join(",") : typeof hostHeader === "number" ? String(hostHeader) : void 0;
|
|
652
1051
|
const url = request.path && host ? `${request.protocol || "http:"}//${host}${request.path}` : void 0;
|
|
1052
|
+
const llmState = createLlmEnrichmentState(span, url, request.method, headers ?? void 0);
|
|
1053
|
+
if (llmState) {
|
|
1054
|
+
updateLlmRequestHeaders(llmState, headers ?? {});
|
|
1055
|
+
this._llmStateByRequest.set(request, llmState);
|
|
1056
|
+
}
|
|
653
1057
|
if (typeof request.path === "string" && request.path.length > 0) {
|
|
654
1058
|
const { path, query } = parseRequestPathAndQuery(request.path);
|
|
655
1059
|
span.setAttribute(ATTR_URL_PATH, path);
|
|
@@ -662,8 +1066,9 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
|
|
|
662
1066
|
const chunkBuffer = toBufferChunk(data);
|
|
663
1067
|
if (chunkBuffer) {
|
|
664
1068
|
requestBodySize += chunkBuffer.length;
|
|
665
|
-
span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestBodySize);
|
|
666
|
-
captureRequestBody(span, chunkBuffer,
|
|
1069
|
+
span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestContentLength !== void 0 ? Math.max(requestBodySize, requestContentLength) : requestBodySize);
|
|
1070
|
+
captureRequestBody(span, chunkBuffer, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url);
|
|
1071
|
+
if (llmState) appendLlmRequestChunk(llmState, chunkBuffer);
|
|
667
1072
|
}
|
|
668
1073
|
return originalWrite(data, ...rest);
|
|
669
1074
|
});
|
|
@@ -671,8 +1076,9 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
|
|
|
671
1076
|
const chunkBuffer = toBufferChunk(data);
|
|
672
1077
|
if (chunkBuffer) {
|
|
673
1078
|
requestBodySize += chunkBuffer.length;
|
|
674
|
-
span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestBodySize);
|
|
675
|
-
captureRequestBody(span, chunkBuffer,
|
|
1079
|
+
span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestContentLength !== void 0 ? Math.max(requestBodySize, requestContentLength) : requestBodySize);
|
|
1080
|
+
captureRequestBody(span, chunkBuffer, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url);
|
|
1081
|
+
if (llmState) appendLlmRequestChunk(llmState, chunkBuffer);
|
|
676
1082
|
}
|
|
677
1083
|
return originalEnd(data, ...rest);
|
|
678
1084
|
});
|
|
@@ -680,39 +1086,45 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
|
|
|
680
1086
|
if (originalRequestHook) originalRequestHook(span, request);
|
|
681
1087
|
};
|
|
682
1088
|
}
|
|
683
|
-
_createResponseHook(originalResponseHook
|
|
1089
|
+
_createResponseHook(originalResponseHook) {
|
|
684
1090
|
return (span, response) => {
|
|
685
1091
|
const headers = extractResponseHeaders(response);
|
|
686
1092
|
if (headers) captureResponseHeaders(span, headers);
|
|
687
1093
|
if (response instanceof IncomingMessage) {
|
|
1094
|
+
const requestForState = response.req instanceof ClientRequest ? response.req : void 0;
|
|
1095
|
+
const llmState = requestForState ? this._llmStateByRequest.get(requestForState) : void 0;
|
|
688
1096
|
const requestPath = response.req instanceof ClientRequest ? extractClientRequestPath(response.req) : void 0;
|
|
689
1097
|
if (requestPath) {
|
|
690
1098
|
const { path, query } = parseRequestPathAndQuery(requestPath);
|
|
691
1099
|
span.setAttribute(ATTR_URL_PATH, path);
|
|
692
1100
|
if (query) span.setAttribute(ATTR_URL_QUERY, query);
|
|
693
1101
|
}
|
|
694
|
-
const maxResponseBodySize = config?.maxResponseBodySize || DEFAULT_MAX_RESPONSE_BODY_SIZE;
|
|
695
1102
|
const url = response.url || void 0;
|
|
696
|
-
|
|
1103
|
+
const chunks = [];
|
|
697
1104
|
let totalSize = 0;
|
|
698
1105
|
span.setAttribute(PingopsSemanticAttributes.HTTP_RESPONSE_BODY_SIZE, 0);
|
|
699
1106
|
const shouldCapture = shouldCaptureResponseBody(url);
|
|
1107
|
+
if (llmState && headers) updateLlmResponseHeaders(llmState, headers);
|
|
700
1108
|
response.prependListener("data", (chunk) => {
|
|
701
1109
|
if (!chunk) return;
|
|
702
1110
|
const chunkBuffer = toBufferChunk(chunk);
|
|
703
1111
|
if (!chunkBuffer) return;
|
|
704
1112
|
totalSize += chunkBuffer.length;
|
|
705
1113
|
span.setAttribute(PingopsSemanticAttributes.HTTP_RESPONSE_BODY_SIZE, totalSize);
|
|
1114
|
+
if (llmState) appendLlmResponseChunk(llmState, chunkBuffer);
|
|
706
1115
|
if (!shouldCapture) return;
|
|
707
|
-
|
|
708
|
-
else chunks = null;
|
|
1116
|
+
chunks.push(chunkBuffer);
|
|
709
1117
|
});
|
|
710
1118
|
let finalized = false;
|
|
711
1119
|
const finalizeCapture = () => {
|
|
712
1120
|
if (finalized) return;
|
|
713
1121
|
finalized = true;
|
|
714
|
-
|
|
715
|
-
|
|
1122
|
+
const contentLength = parseContentLength(headers?.["content-length"]);
|
|
1123
|
+
const responseBodySize = contentLength !== void 0 ? Math.max(totalSize, contentLength) : totalSize;
|
|
1124
|
+
span.setAttribute(PingopsSemanticAttributes.HTTP_RESPONSE_BODY_SIZE, responseBodySize);
|
|
1125
|
+
captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, headers, url);
|
|
1126
|
+
if (llmState) finalizeLlmEnrichment(llmState);
|
|
1127
|
+
if (requestForState) this._llmStateByRequest.delete(requestForState);
|
|
716
1128
|
};
|
|
717
1129
|
response.prependOnceListener("end", finalizeCapture);
|
|
718
1130
|
response.prependOnceListener("close", finalizeCapture);
|
|
@@ -744,7 +1156,6 @@ function toRequestUrl$1(request) {
|
|
|
744
1156
|
* @returns PingopsHttpInstrumentation instance
|
|
745
1157
|
*/
|
|
746
1158
|
function createHttpInstrumentation(config) {
|
|
747
|
-
const globalConfig$1 = getGlobalConfig();
|
|
748
1159
|
const userIgnoreOutgoingRequestHook = config?.ignoreOutgoingRequestHook;
|
|
749
1160
|
return new PingopsHttpInstrumentation({
|
|
750
1161
|
...config,
|
|
@@ -752,9 +1163,7 @@ function createHttpInstrumentation(config) {
|
|
|
752
1163
|
ignoreOutgoingRequestHook: (request) => {
|
|
753
1164
|
if (shouldIgnoreOutboundInstrumentation(toRequestUrl$1(request))) return true;
|
|
754
1165
|
return userIgnoreOutgoingRequestHook?.(request) ?? false;
|
|
755
|
-
}
|
|
756
|
-
maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
|
|
757
|
-
maxResponseBodySize: globalConfig$1?.maxResponseBodySize
|
|
1166
|
+
}
|
|
758
1167
|
});
|
|
759
1168
|
}
|
|
760
1169
|
|
|
@@ -879,7 +1288,10 @@ var UndiciInstrumentation = class extends InstrumentationBase {
|
|
|
879
1288
|
const serverPort = requestUrl.port || schemePorts[urlScheme];
|
|
880
1289
|
attributes[ATTR_SERVER_ADDRESS] = serverAddress;
|
|
881
1290
|
if (serverPort && !isNaN(Number(serverPort))) attributes[ATTR_SERVER_PORT] = Number(serverPort);
|
|
882
|
-
const
|
|
1291
|
+
const headersMap = this.parseRequestHeaders(request);
|
|
1292
|
+
const requestHeadersObject = {};
|
|
1293
|
+
for (const [key, value] of headersMap.entries()) requestHeadersObject[key] = value;
|
|
1294
|
+
const userAgentValues = headersMap.get("user-agent");
|
|
883
1295
|
if (userAgentValues) attributes[ATTR_USER_AGENT_ORIGINAL] = Array.isArray(userAgentValues) ? userAgentValues[userAgentValues.length - 1] : userAgentValues;
|
|
884
1296
|
const hookAttributes = safeExecuteInTheMiddle(() => config.startSpanHook?.(request), (e) => e && this._diag.error("caught startSpanHook error: ", e), true);
|
|
885
1297
|
if (hookAttributes) Object.entries(hookAttributes).forEach(([key, val]) => {
|
|
@@ -912,17 +1324,16 @@ var UndiciInstrumentation = class extends InstrumentationBase {
|
|
|
912
1324
|
responseBodyChunks: [],
|
|
913
1325
|
requestBodySize: 0,
|
|
914
1326
|
responseBodySize: 0,
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
requestBodyCaptureExceeded: false,
|
|
918
|
-
responseBodyCaptureExceeded: false,
|
|
919
|
-
url: requestUrl.toString()
|
|
1327
|
+
url: requestUrl.toString(),
|
|
1328
|
+
llmState: createLlmEnrichmentState(span, requestUrl.toString(), request.method, requestHeadersObject)
|
|
920
1329
|
});
|
|
1330
|
+
const createdRecord = this._recordFromReq.get(request);
|
|
1331
|
+
if (createdRecord?.llmState) updateLlmRequestHeaders(createdRecord.llmState, requestHeadersObject);
|
|
921
1332
|
}
|
|
922
1333
|
onRequestHeaders({ request, socket }) {
|
|
923
1334
|
const record = this._recordFromReq.get(request);
|
|
924
1335
|
if (!record) return;
|
|
925
|
-
const { span } = record;
|
|
1336
|
+
const { span, attributes } = record;
|
|
926
1337
|
const spanAttributes = {};
|
|
927
1338
|
const remoteAddress = typeof socket.remoteAddress === "string" ? socket.remoteAddress : void 0;
|
|
928
1339
|
const remotePort = typeof socket.remotePort === "number" ? socket.remotePort : void 0;
|
|
@@ -934,12 +1345,19 @@ var UndiciInstrumentation = class extends InstrumentationBase {
|
|
|
934
1345
|
spanAttributes[`http.request.header.${name}`] = attrValue;
|
|
935
1346
|
}
|
|
936
1347
|
span.setAttributes(spanAttributes);
|
|
1348
|
+
record.attributes = Object.assign(attributes, spanAttributes);
|
|
1349
|
+
if (record.llmState) {
|
|
1350
|
+
const requestHeadersObject = {};
|
|
1351
|
+
for (const [key, value] of headersMap.entries()) requestHeadersObject[key] = value;
|
|
1352
|
+
updateLlmRequestHeaders(record.llmState, requestHeadersObject);
|
|
1353
|
+
}
|
|
937
1354
|
}
|
|
938
1355
|
onResponseHeaders({ request, response }) {
|
|
939
1356
|
const record = this._recordFromReq.get(request);
|
|
940
1357
|
if (!record) return;
|
|
941
1358
|
const { span, attributes } = record;
|
|
942
1359
|
const spanAttributes = { [ATTR_HTTP_RESPONSE_STATUS_CODE]: response.statusCode };
|
|
1360
|
+
const responseHeadersObject = {};
|
|
943
1361
|
const config = this.getConfig();
|
|
944
1362
|
safeExecuteInTheMiddle(() => config.responseHook?.(span, {
|
|
945
1363
|
request,
|
|
@@ -949,6 +1367,7 @@ var UndiciInstrumentation = class extends InstrumentationBase {
|
|
|
949
1367
|
const name = response.headers[idx].toString().toLowerCase();
|
|
950
1368
|
const value = response.headers[idx + 1];
|
|
951
1369
|
spanAttributes[`http.response.header.${name}`] = value.toString();
|
|
1370
|
+
responseHeadersObject[name] = value.toString();
|
|
952
1371
|
if (name === "content-length") {
|
|
953
1372
|
const contentLength = Number(value.toString());
|
|
954
1373
|
if (!isNaN(contentLength)) spanAttributes["http.response.header.content-length"] = contentLength;
|
|
@@ -957,31 +1376,33 @@ var UndiciInstrumentation = class extends InstrumentationBase {
|
|
|
957
1376
|
span.setAttributes(spanAttributes);
|
|
958
1377
|
span.setStatus({ code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET });
|
|
959
1378
|
record.attributes = Object.assign(attributes, spanAttributes);
|
|
1379
|
+
if (record.llmState) updateLlmResponseHeaders(record.llmState, responseHeadersObject);
|
|
960
1380
|
}
|
|
961
1381
|
onDone({ request }) {
|
|
962
1382
|
const record = this._recordFromReq.get(request);
|
|
963
1383
|
if (!record) return;
|
|
964
1384
|
const { span, attributes, startTime } = record;
|
|
965
|
-
|
|
966
|
-
|
|
1385
|
+
const requestContentLength = parseContentLength(record.attributes?.["http.request.header.content-length"]);
|
|
1386
|
+
const effectiveRequestBodySize = requestContentLength !== void 0 ? Math.max(record.requestBodySize, requestContentLength) : record.requestBodySize;
|
|
1387
|
+
const responseContentLength = parseContentLength(record.attributes?.["http.response.header.content-length"]);
|
|
1388
|
+
const effectiveResponseBodySize = responseContentLength !== void 0 ? Math.max(record.responseBodySize, responseContentLength) : record.responseBodySize;
|
|
1389
|
+
span.setAttribute(HTTP_REQUEST_BODY_SIZE, effectiveRequestBodySize);
|
|
1390
|
+
span.setAttribute(HTTP_RESPONSE_BODY_SIZE, effectiveResponseBodySize);
|
|
967
1391
|
if (shouldCaptureResponseBody(record.url)) {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
span.setAttribute(
|
|
976
|
-
if (contentEncoding) span.setAttribute(HTTP_RESPONSE_CONTENT_ENCODING, contentEncoding);
|
|
977
|
-
} else {
|
|
978
|
-
const bodyStr = bufferToBodyString(responseBodyBuffer);
|
|
979
|
-
if (bodyStr != null) span.setAttribute(HTTP_RESPONSE_BODY, bodyStr);
|
|
1392
|
+
if (record.responseBodyChunks.length > 0) try {
|
|
1393
|
+
const encoded = encodeBodyBufferForSpan(Buffer.concat(record.responseBodyChunks), {
|
|
1394
|
+
"content-encoding": record.attributes?.["http.response.header.content-encoding"],
|
|
1395
|
+
"content-type": record.attributes?.["http.response.header.content-type"]
|
|
1396
|
+
});
|
|
1397
|
+
if (encoded) {
|
|
1398
|
+
span.setAttribute(HTTP_RESPONSE_BODY, encoded.content);
|
|
1399
|
+
if (encoded.contentEncoding) span.setAttribute(HTTP_RESPONSE_CONTENT_ENCODING, encoded.contentEncoding);
|
|
980
1400
|
}
|
|
981
1401
|
} catch (e) {
|
|
982
1402
|
this._diag.error("Error occurred while capturing response body:", e);
|
|
983
1403
|
}
|
|
984
1404
|
}
|
|
1405
|
+
if (record.llmState) finalizeLlmEnrichment(record.llmState);
|
|
985
1406
|
span.end();
|
|
986
1407
|
this._recordFromReq.delete(request);
|
|
987
1408
|
this.recordRequestDuration(attributes, startTime);
|
|
@@ -990,12 +1411,19 @@ var UndiciInstrumentation = class extends InstrumentationBase {
|
|
|
990
1411
|
const record = this._recordFromReq.get(request);
|
|
991
1412
|
if (!record) return;
|
|
992
1413
|
const { span, attributes, startTime } = record;
|
|
993
|
-
|
|
994
|
-
|
|
1414
|
+
const requestContentLength = parseContentLength(record.attributes?.["http.request.header.content-length"]);
|
|
1415
|
+
const effectiveRequestBodySize = requestContentLength !== void 0 ? Math.max(record.requestBodySize, requestContentLength) : record.requestBodySize;
|
|
1416
|
+
const responseContentLength = parseContentLength(record.attributes?.["http.response.header.content-length"]);
|
|
1417
|
+
const effectiveResponseBodySize = responseContentLength !== void 0 ? Math.max(record.responseBodySize, responseContentLength) : record.responseBodySize;
|
|
1418
|
+
span.setAttribute(HTTP_REQUEST_BODY_SIZE, effectiveRequestBodySize);
|
|
1419
|
+
span.setAttribute(HTTP_RESPONSE_BODY_SIZE, effectiveResponseBodySize);
|
|
995
1420
|
if (shouldCaptureRequestBody(record.url)) {
|
|
996
|
-
if (record.requestBodyChunks.length > 0
|
|
997
|
-
const
|
|
998
|
-
|
|
1421
|
+
if (record.requestBodyChunks.length > 0) try {
|
|
1422
|
+
const encoded = encodeBodyBufferForSpan(Buffer.concat(record.requestBodyChunks), {
|
|
1423
|
+
"content-encoding": record.attributes?.["http.request.header.content-encoding"],
|
|
1424
|
+
"content-type": record.attributes?.["http.request.header.content-type"]
|
|
1425
|
+
});
|
|
1426
|
+
if (encoded?.content) span.setAttribute(HTTP_REQUEST_BODY, encoded.content);
|
|
999
1427
|
} catch (e) {
|
|
1000
1428
|
this._diag.error("Error occurred while capturing request body:", e);
|
|
1001
1429
|
}
|
|
@@ -1006,6 +1434,7 @@ var UndiciInstrumentation = class extends InstrumentationBase {
|
|
|
1006
1434
|
code: SpanStatusCode.ERROR,
|
|
1007
1435
|
message: errorMessage
|
|
1008
1436
|
});
|
|
1437
|
+
if (record.llmState) finalizeLlmEnrichment(record.llmState);
|
|
1009
1438
|
span.end();
|
|
1010
1439
|
this._recordFromReq.delete(request);
|
|
1011
1440
|
attributes[ATTR_ERROR_TYPE] = errorMessage;
|
|
@@ -1015,54 +1444,38 @@ var UndiciInstrumentation = class extends InstrumentationBase {
|
|
|
1015
1444
|
const record = this._recordFromReq.get(request);
|
|
1016
1445
|
if (!record) return;
|
|
1017
1446
|
record.requestBodySize += chunk.length;
|
|
1447
|
+
if (record.llmState) appendLlmRequestChunk(record.llmState, chunk);
|
|
1018
1448
|
if (!shouldCaptureRequestBody(record.url)) return;
|
|
1019
|
-
|
|
1020
|
-
if (!record.requestBodyCaptureExceeded && record.requestBodyCaptureSize + chunk.length <= maxRequestBodySize) {
|
|
1021
|
-
record.requestBodyChunks.push(chunk);
|
|
1022
|
-
record.requestBodyCaptureSize += chunk.length;
|
|
1023
|
-
} else {
|
|
1024
|
-
record.requestBodyCaptureExceeded = true;
|
|
1025
|
-
record.requestBodyChunks = [];
|
|
1026
|
-
record.requestBodyCaptureSize = 0;
|
|
1027
|
-
}
|
|
1449
|
+
record.requestBodyChunks.push(chunk);
|
|
1028
1450
|
}
|
|
1029
1451
|
onBodySent({ request }) {
|
|
1030
1452
|
const record = this._recordFromReq.get(request);
|
|
1031
1453
|
if (!record) return;
|
|
1032
|
-
record.
|
|
1454
|
+
const requestContentLength = parseContentLength(record.attributes?.["http.request.header.content-length"]);
|
|
1455
|
+
const effectiveRequestBodySize = requestContentLength !== void 0 ? Math.max(record.requestBodySize, requestContentLength) : record.requestBodySize;
|
|
1456
|
+
record.span.setAttribute(HTTP_REQUEST_BODY_SIZE, effectiveRequestBodySize);
|
|
1033
1457
|
if (!shouldCaptureRequestBody(record.url)) {
|
|
1034
1458
|
record.requestBodyChunks = [];
|
|
1035
|
-
record.requestBodyCaptureSize = 0;
|
|
1036
|
-
record.requestBodyCaptureExceeded = false;
|
|
1037
1459
|
return;
|
|
1038
1460
|
}
|
|
1039
|
-
if (record.
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
if (
|
|
1461
|
+
if (record.requestBodyChunks.length > 0) try {
|
|
1462
|
+
const encoded = encodeBodyBufferForSpan(Buffer.concat(record.requestBodyChunks), {
|
|
1463
|
+
"content-encoding": record.attributes?.["http.request.header.content-encoding"],
|
|
1464
|
+
"content-type": record.attributes?.["http.request.header.content-type"]
|
|
1465
|
+
});
|
|
1466
|
+
if (encoded?.content) record.span.setAttribute(HTTP_REQUEST_BODY, encoded.content);
|
|
1045
1467
|
} catch (e) {
|
|
1046
1468
|
this._diag.error("Error occurred while capturing request body:", e);
|
|
1047
1469
|
}
|
|
1048
1470
|
record.requestBodyChunks = [];
|
|
1049
|
-
record.requestBodyCaptureSize = 0;
|
|
1050
|
-
record.requestBodyCaptureExceeded = false;
|
|
1051
1471
|
}
|
|
1052
1472
|
onBodyChunkReceived({ request, chunk }) {
|
|
1053
1473
|
const record = this._recordFromReq.get(request);
|
|
1054
1474
|
if (!record) return;
|
|
1055
1475
|
record.responseBodySize += chunk.length;
|
|
1476
|
+
if (record.llmState) appendLlmResponseChunk(record.llmState, chunk);
|
|
1056
1477
|
if (!shouldCaptureResponseBody(record.url)) return;
|
|
1057
|
-
|
|
1058
|
-
if (!record.responseBodyCaptureExceeded && record.responseBodyCaptureSize + chunk.length <= maxResponseBodySize) {
|
|
1059
|
-
record.responseBodyChunks.push(chunk);
|
|
1060
|
-
record.responseBodyCaptureSize += chunk.length;
|
|
1061
|
-
} else {
|
|
1062
|
-
record.responseBodyCaptureExceeded = true;
|
|
1063
|
-
record.responseBodyChunks = [];
|
|
1064
|
-
record.responseBodyCaptureSize = 0;
|
|
1065
|
-
}
|
|
1478
|
+
record.responseBodyChunks.push(chunk);
|
|
1066
1479
|
}
|
|
1067
1480
|
recordRequestDuration(attributes, startTime) {
|
|
1068
1481
|
const metricsAttributes = {};
|
|
@@ -1114,14 +1527,11 @@ function toRequestUrl(request) {
|
|
|
1114
1527
|
* @returns UndiciInstrumentation instance
|
|
1115
1528
|
*/
|
|
1116
1529
|
function createUndiciInstrumentation() {
|
|
1117
|
-
const globalConfig$1 = getGlobalConfig();
|
|
1118
1530
|
return new UndiciInstrumentation({
|
|
1119
1531
|
enabled: true,
|
|
1120
1532
|
ignoreRequestHook: (request) => {
|
|
1121
1533
|
return shouldIgnoreOutboundInstrumentation(toRequestUrl(request));
|
|
1122
|
-
}
|
|
1123
|
-
maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
|
|
1124
|
-
maxResponseBodySize: globalConfig$1?.maxResponseBodySize
|
|
1534
|
+
}
|
|
1125
1535
|
});
|
|
1126
1536
|
}
|
|
1127
1537
|
|