@pingops/otel 0.2.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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, bufferToBodyString, createLogger, extractSpanPayload, getHttpUrlFromAttributes, getPropagatedAttributesFromContext, isCompressedContentEncoding, isSpanEligible, shouldCaptureSpan } from "@pingops/core";
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$2 = createLogger("[PingOps Processor]");
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, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction) {
65
- const payload = extractSpanPayload(span, domainAllowList, globalHeadersAllowList, globalHeadersDenyList, globalCaptureRequestBody, globalCaptureResponseBody, headerRedaction);
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$2.debug("Payload", { payload });
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];
@@ -106,31 +107,28 @@ var PingopsSpanProcessor = class {
106
107
  });
107
108
  this.config = {
108
109
  debug: config.debug ?? false,
109
- headersAllowList: config.headersAllowList,
110
- headersDenyList: config.headersDenyList,
110
+ sdkVersion: config.sdkVersion,
111
111
  domainAllowList: config.domainAllowList,
112
112
  domainDenyList: config.domainDenyList,
113
113
  captureRequestBody: config.captureRequestBody,
114
114
  captureResponseBody: config.captureResponseBody,
115
- headerRedaction: config.headerRedaction
115
+ transforms: config.transforms
116
116
  };
117
117
  setGlobalConfig({
118
118
  captureRequestBody: config.captureRequestBody,
119
119
  captureResponseBody: config.captureResponseBody,
120
120
  domainAllowList: config.domainAllowList,
121
- maxRequestBodySize: config.maxRequestBodySize,
122
- maxResponseBodySize: config.maxResponseBodySize,
123
- exportTraceUrl: this.exporterTraceUrl
121
+ exportTraceUrl: this.exporterTraceUrl,
122
+ llmMonitoring: config.llmMonitoring
124
123
  });
125
- logger$2.info("Initialized PingopsSpanProcessor", {
124
+ logger$3.info("Initialized PingopsSpanProcessor", {
126
125
  baseUrl: config.baseUrl,
127
126
  exportMode,
128
127
  batchSize: config.batchSize,
129
128
  batchTimeout: config.batchTimeout,
130
129
  hasDomainAllowList: !!config.domainAllowList && config.domainAllowList.length > 0,
131
130
  hasDomainDenyList: !!config.domainDenyList && config.domainDenyList.length > 0,
132
- hasHeadersAllowList: !!config.headersAllowList && config.headersAllowList.length > 0,
133
- hasHeadersDenyList: !!config.headersDenyList && config.headersDenyList.length > 0
131
+ hasTransforms: !!config.transforms
134
132
  });
135
133
  }
136
134
  /**
@@ -138,15 +136,16 @@ var PingopsSpanProcessor = class {
138
136
  */
139
137
  onStart(span, parentContext) {
140
138
  const spanContext = span.spanContext();
141
- logger$2.debug("Span started", {
139
+ logger$3.debug("Span started", {
142
140
  spanName: span.name,
143
141
  spanId: spanContext.spanId,
144
142
  traceId: spanContext.traceId
145
143
  });
144
+ if (this.config.sdkVersion) span.setAttribute("pingops.sdk.version", this.config.sdkVersion);
146
145
  const propagatedAttributes = getPropagatedAttributesFromContext(parentContext);
147
146
  if (Object.keys(propagatedAttributes).length > 0) {
148
147
  for (const [key, value] of Object.entries(propagatedAttributes)) if (typeof value === "string" || Array.isArray(value)) span.setAttribute(key, value);
149
- logger$2.debug("Set propagated attributes on span", {
148
+ logger$3.debug("Set propagated attributes on span", {
150
149
  spanName: span.name,
151
150
  attributeKeys: Object.keys(propagatedAttributes)
152
151
  });
@@ -164,7 +163,7 @@ var PingopsSpanProcessor = class {
164
163
  */
165
164
  onEnd(span) {
166
165
  const spanContext = span.spanContext();
167
- logger$2.debug("Span ended, processing", {
166
+ logger$3.debug("Span ended, processing", {
168
167
  spanName: span.name,
169
168
  spanId: spanContext.spanId,
170
169
  traceId: spanContext.traceId,
@@ -172,7 +171,7 @@ var PingopsSpanProcessor = class {
172
171
  });
173
172
  try {
174
173
  if (!isSpanEligible(span)) {
175
- logger$2.debug("Span not eligible, skipping", {
174
+ logger$3.debug("Span not eligible, skipping", {
176
175
  spanName: span.name,
177
176
  spanId: spanContext.spanId,
178
177
  reason: "not CLIENT or missing HTTP/GenAI attributes"
@@ -182,14 +181,14 @@ var PingopsSpanProcessor = class {
182
181
  const attributes = span.attributes;
183
182
  const url = getHttpUrlFromAttributes(attributes) ?? "";
184
183
  if (url && isExporterRequestUrl$1(url, this.exporterTraceUrl)) {
185
- logger$2.debug("Skipping exporter span to prevent self-instrumentation", {
184
+ logger$3.debug("Skipping exporter span to prevent self-instrumentation", {
186
185
  spanName: span.name,
187
186
  spanId: spanContext.spanId,
188
187
  url
189
188
  });
190
189
  return;
191
190
  }
192
- logger$2.debug("Extracted URL for domain filtering", {
191
+ logger$3.debug("Extracted URL for domain filtering", {
193
192
  spanName: span.name,
194
193
  url,
195
194
  hasHttpUrl: !!attributes["http.url"],
@@ -198,25 +197,25 @@ var PingopsSpanProcessor = class {
198
197
  });
199
198
  if (url) {
200
199
  if (!shouldCaptureSpan(url, this.config.domainAllowList, this.config.domainDenyList)) {
201
- logger$2.info("Span filtered out by domain rules", {
200
+ logger$3.info("Span filtered out by domain rules", {
202
201
  spanName: span.name,
203
202
  spanId: spanContext.spanId,
204
203
  url
205
204
  });
206
205
  return;
207
206
  }
208
- } else logger$2.debug("No URL found for domain filtering, proceeding", { spanName: span.name });
209
- const filteredSpan = createFilteredSpan(span, this.config.domainAllowList, this.config.headersAllowList, this.config.headersDenyList, this.config.captureRequestBody, this.config.captureResponseBody, this.config.headerRedaction);
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);
210
209
  this.processor.onEnd(filteredSpan);
211
- logger$2.info("Span passed all filters and queued for export", {
210
+ logger$3.info("Span passed all filters and queued for export", {
212
211
  spanName: span.name,
213
212
  spanId: spanContext.spanId,
214
213
  traceId: spanContext.traceId,
215
214
  url,
216
- hasHeaderFiltering: !!(this.config.headersAllowList || this.config.headersDenyList)
215
+ hasTransforms: !!this.config.transforms
217
216
  });
218
217
  } catch (error) {
219
- logger$2.error("Error processing span", {
218
+ logger$3.error("Error processing span", {
220
219
  spanName: span.name,
221
220
  spanId: spanContext.spanId,
222
221
  error: error instanceof Error ? error.message : String(error)
@@ -229,12 +228,12 @@ var PingopsSpanProcessor = class {
229
228
  * @returns Promise that resolves when all pending operations are complete
230
229
  */
231
230
  async forceFlush() {
232
- logger$2.info("Force flushing spans");
231
+ logger$3.info("Force flushing spans");
233
232
  try {
234
233
  await this.processor.forceFlush();
235
- logger$2.info("Force flush complete");
234
+ logger$3.info("Force flush complete");
236
235
  } catch (error) {
237
- logger$2.error("Error during force flush", { error: error instanceof Error ? error.message : String(error) });
236
+ logger$3.error("Error during force flush", { error: error instanceof Error ? error.message : String(error) });
238
237
  throw error;
239
238
  }
240
239
  }
@@ -244,12 +243,12 @@ var PingopsSpanProcessor = class {
244
243
  * @returns Promise that resolves when shutdown is complete
245
244
  */
246
245
  async shutdown() {
247
- logger$2.info("Shutting down processor");
246
+ logger$3.info("Shutting down processor");
248
247
  try {
249
248
  await this.processor.shutdown();
250
- logger$2.info("Processor shutdown complete");
249
+ logger$3.info("Processor shutdown complete");
251
250
  } catch (error) {
252
- logger$2.error("Error during processor shutdown", { error: error instanceof Error ? error.message : String(error) });
251
+ logger$3.error("Error during processor shutdown", { error: error instanceof Error ? error.message : String(error) });
253
252
  throw error;
254
253
  }
255
254
  }
@@ -264,7 +263,7 @@ const PINGOPS_GLOBAL_SYMBOL = Symbol.for("pingops");
264
263
  /**
265
264
  * Logger instance for tracer provider
266
265
  */
267
- const logger$1 = createLogger("[PingOps TracerProvider]");
266
+ const logger$2 = createLogger("[PingOps TracerProvider]");
268
267
  /**
269
268
  * Creates initial global state
270
269
  */
@@ -279,21 +278,21 @@ function getGlobalState() {
279
278
  try {
280
279
  const g = globalThis;
281
280
  if (typeof g !== "object" || g === null) {
282
- logger$1.warn("globalThis is not available, using fallback state");
281
+ logger$2.warn("globalThis is not available, using fallback state");
283
282
  return initialState;
284
283
  }
285
284
  if (!g[PINGOPS_GLOBAL_SYMBOL]) {
286
- logger$1.debug("Creating new global state");
285
+ logger$2.debug("Creating new global state");
287
286
  Object.defineProperty(g, PINGOPS_GLOBAL_SYMBOL, {
288
287
  value: initialState,
289
288
  writable: false,
290
289
  configurable: false,
291
290
  enumerable: false
292
291
  });
293
- } else logger$1.debug("Retrieved existing global state");
292
+ } else logger$2.debug("Retrieved existing global state");
294
293
  return g[PINGOPS_GLOBAL_SYMBOL];
295
294
  } catch (err) {
296
- logger$1.error("Failed to access global state:", err instanceof Error ? err.message : String(err));
295
+ logger$2.error("Failed to access global state:", err instanceof Error ? err.message : String(err));
297
296
  return initialState;
298
297
  }
299
298
  }
@@ -311,11 +310,11 @@ function setPingopsTracerProvider(provider) {
311
310
  const state = getGlobalState();
312
311
  const hadProvider = state.isolatedTracerProvider !== null;
313
312
  state.isolatedTracerProvider = provider;
314
- if (provider) logger$1.info("Set isolated TracerProvider", {
313
+ if (provider) logger$2.info("Set isolated TracerProvider", {
315
314
  hadPrevious: hadProvider,
316
315
  providerType: provider.constructor.name
317
316
  });
318
- else logger$1.info("Cleared isolated TracerProvider", { hadPrevious: hadProvider });
317
+ else logger$2.info("Cleared isolated TracerProvider", { hadPrevious: hadProvider });
319
318
  }
320
319
  /**
321
320
  * Gets the TracerProvider for PingOps tracing operations.
@@ -329,36 +328,36 @@ function setPingopsTracerProvider(provider) {
329
328
  function getPingopsTracerProvider() {
330
329
  const { isolatedTracerProvider } = getGlobalState();
331
330
  if (isolatedTracerProvider) {
332
- logger$1.debug("Using isolated TracerProvider", { providerType: isolatedTracerProvider.constructor.name });
331
+ logger$2.debug("Using isolated TracerProvider", { providerType: isolatedTracerProvider.constructor.name });
333
332
  return isolatedTracerProvider;
334
333
  }
335
334
  const globalProvider = trace.getTracerProvider();
336
- logger$1.debug("Using global TracerProvider", { providerType: globalProvider.constructor.name });
335
+ logger$2.debug("Using global TracerProvider", { providerType: globalProvider.constructor.name });
337
336
  return globalProvider;
338
337
  }
339
338
  /**
340
339
  * Shuts down the TracerProvider and flushes remaining spans
341
340
  */
342
341
  async function shutdownTracerProvider() {
343
- logger$1.info("Shutting down TracerProvider");
342
+ logger$2.info("Shutting down TracerProvider");
344
343
  const providerWithShutdown = getPingopsTracerProvider();
345
344
  if (providerWithShutdown && typeof providerWithShutdown.shutdown === "function") {
346
- logger$1.debug("Calling provider.shutdown()");
345
+ logger$2.debug("Calling provider.shutdown()");
347
346
  try {
348
347
  await providerWithShutdown.shutdown();
349
- logger$1.info("TracerProvider shutdown complete");
348
+ logger$2.info("TracerProvider shutdown complete");
350
349
  } catch (error) {
351
- logger$1.error("Error during TracerProvider shutdown:", error instanceof Error ? error.message : String(error));
350
+ logger$2.error("Error during TracerProvider shutdown:", error instanceof Error ? error.message : String(error));
352
351
  throw error;
353
352
  }
354
- } else logger$1.warn("TracerProvider does not have shutdown method, skipping");
353
+ } else logger$2.warn("TracerProvider does not have shutdown method, skipping");
355
354
  setPingopsTracerProvider(null);
356
- logger$1.info("TracerProvider shutdown finished");
355
+ logger$2.info("TracerProvider shutdown finished");
357
356
  }
358
357
 
359
358
  //#endregion
360
359
  //#region src/instrumentations/suppression-guard.ts
361
- const logger = createLogger("[PingOps SuppressionGuard]");
360
+ const logger$1 = createLogger("[PingOps SuppressionGuard]");
362
361
  let hasLoggedSuppressionLeakWarning = false;
363
362
  function normalizePath(pathname) {
364
363
  return pathname.replace(/\/+/g, "/").replace(/\/$/, "");
@@ -395,26 +394,496 @@ function shouldIgnoreOutboundInstrumentation(requestUrl) {
395
394
  */
396
395
  function resolveOutboundSpanParentContext(activeContext, requestUrl) {
397
396
  if (!isTracingSuppressed(activeContext)) return activeContext;
397
+ if (activeContext.getValue(PINGOPS_INTENTIONAL_SUPPRESSION) === true) return activeContext;
398
398
  if (isExporterRequestUrl(requestUrl)) return activeContext;
399
399
  if (!hasLoggedSuppressionLeakWarning) {
400
- 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");
401
401
  hasLoggedSuppressionLeakWarning = true;
402
- } 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");
403
403
  return ROOT_CONTEXT;
404
404
  }
405
405
 
406
+ //#endregion
407
+ //#region src/instrumentations/body-utils.ts
408
+ const HTTP_REQUEST_BODY = "http.request.body";
409
+ const HTTP_RESPONSE_BODY = "http.response.body";
410
+ const HTTP_REQUEST_BODY_SIZE = "http.request.body.size";
411
+ const HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";
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
+ ]);
438
+ /**
439
+ * Gets domain rule configuration for a given URL.
440
+ */
441
+ function getDomainRule(url, domainAllowList) {
442
+ if (!domainAllowList) return;
443
+ const domain = extractDomainFromUrl(url);
444
+ for (const rule of domainAllowList) if (domain === rule.domain || domain.endsWith(`.${rule.domain}`) || domain === rule.domain.slice(1)) return rule;
445
+ }
446
+ /**
447
+ * Determines if request body should be captured based on priority:
448
+ * context > domain rule > global config > default (false).
449
+ */
450
+ function shouldCaptureRequestBody(url) {
451
+ const contextValue = context.active().getValue(PINGOPS_CAPTURE_REQUEST_BODY);
452
+ if (contextValue !== void 0) return contextValue;
453
+ if (url) {
454
+ const domainRule = getDomainRule(url, getGlobalConfig()?.domainAllowList);
455
+ if (domainRule?.captureRequestBody !== void 0) return domainRule.captureRequestBody;
456
+ }
457
+ const globalConfig$1 = getGlobalConfig();
458
+ if (globalConfig$1?.captureRequestBody !== void 0) return globalConfig$1.captureRequestBody;
459
+ return false;
460
+ }
461
+ /**
462
+ * Determines if response body should be captured based on priority:
463
+ * context > domain rule > global config > default (false).
464
+ */
465
+ function shouldCaptureResponseBody(url) {
466
+ const contextValue = context.active().getValue(PINGOPS_CAPTURE_RESPONSE_BODY);
467
+ if (contextValue !== void 0) return contextValue;
468
+ if (url) {
469
+ const domainRule = getDomainRule(url, getGlobalConfig()?.domainAllowList);
470
+ if (domainRule?.captureResponseBody !== void 0) return domainRule.captureResponseBody;
471
+ }
472
+ const globalConfig$1 = getGlobalConfig();
473
+ if (globalConfig$1?.captureResponseBody !== void 0) return globalConfig$1.captureResponseBody;
474
+ return false;
475
+ }
476
+ /**
477
+ * Normalizes supported HTTP chunk types into a Buffer.
478
+ */
479
+ function toBufferChunk(data) {
480
+ if (typeof data === "string") return Buffer.from(data);
481
+ if (Buffer.isBuffer(data)) return data;
482
+ if (data instanceof Uint8Array) return Buffer.from(data);
483
+ return null;
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
+ }
874
+
406
875
  //#endregion
407
876
  //#region src/instrumentations/http/pingops-http.ts
408
877
  /**
409
878
  * Pingops HTTP instrumentation that extends HttpInstrumentation
410
879
  * with request/response body capture
411
880
  */
412
- const DEFAULT_MAX_REQUEST_BODY_SIZE$1 = 4 * 1024;
413
- const DEFAULT_MAX_RESPONSE_BODY_SIZE$1 = 4 * 1024;
414
881
  const LEGACY_ATTR_HTTP_URL = "http.url";
415
882
  const PingopsSemanticAttributes = {
416
- HTTP_REQUEST_BODY: "http.request.body",
417
- HTTP_RESPONSE_BODY: "http.response.body"
883
+ HTTP_REQUEST_BODY,
884
+ HTTP_RESPONSE_BODY,
885
+ HTTP_REQUEST_BODY_SIZE,
886
+ HTTP_RESPONSE_BODY_SIZE
418
887
  };
419
888
  /**
420
889
  * Manually flattens a nested object into dot-notation keys
@@ -427,7 +896,7 @@ function isPrimitiveArray(value) {
427
896
  }
428
897
  function flatten(obj, prefix = "") {
429
898
  const result = {};
430
- for (const key in obj) if (Object.prototype.hasOwnProperty.call(obj, key)) {
899
+ for (const key in obj) if (Object.hasOwn(obj, key)) {
431
900
  const newKey = prefix ? `${prefix}.${key}` : key;
432
901
  const value = obj[key];
433
902
  if (isPlainObject(value)) Object.assign(result, flatten(value, newKey));
@@ -446,61 +915,12 @@ function setAttributeValue(span, attrName, attrValue) {
446
915
  } else if (isPlainObject(attrValue)) span.setAttributes(flatten({ [attrName]: attrValue }));
447
916
  }
448
917
  /**
449
- * Extracts domain from URL
450
- */
451
- function extractDomainFromUrl$1(url) {
452
- try {
453
- return new URL(url).hostname;
454
- } catch {
455
- const match = url.match(/^(?:https?:\/\/)?([^/]+)/);
456
- return match ? match[1] : "";
457
- }
458
- }
459
- /**
460
- * Gets domain rule configuration for a given URL
461
- */
462
- function getDomainRule$1(url, domainAllowList) {
463
- if (!domainAllowList) return;
464
- const domain = extractDomainFromUrl$1(url);
465
- for (const rule of domainAllowList) if (domain === rule.domain || domain.endsWith(`.${rule.domain}`) || domain === rule.domain.slice(1)) return rule;
466
- }
467
- /**
468
- * Determines if request body should be captured based on priority:
469
- * context > domain rule > global config > default (false)
918
+ * Captures request body from a chunk buffer.
470
919
  */
471
- function shouldCaptureRequestBody$1(url) {
472
- const contextValue = context.active().getValue(PINGOPS_CAPTURE_REQUEST_BODY);
473
- if (contextValue !== void 0) return contextValue;
474
- if (url) {
475
- const domainRule = getDomainRule$1(url, getGlobalConfig()?.domainAllowList);
476
- if (domainRule?.captureRequestBody !== void 0) return domainRule.captureRequestBody;
477
- }
478
- const globalConfig$1 = getGlobalConfig();
479
- if (globalConfig$1?.captureRequestBody !== void 0) return globalConfig$1.captureRequestBody;
480
- return false;
481
- }
482
- /**
483
- * Determines if response body should be captured based on priority:
484
- * context > domain rule > global config > default (false)
485
- */
486
- function shouldCaptureResponseBody$1(url) {
487
- const contextValue = context.active().getValue(PINGOPS_CAPTURE_RESPONSE_BODY);
488
- if (contextValue !== void 0) return contextValue;
489
- if (url) {
490
- const domainRule = getDomainRule$1(url, getGlobalConfig()?.domainAllowList);
491
- if (domainRule?.captureResponseBody !== void 0) return domainRule.captureResponseBody;
492
- }
493
- const globalConfig$1 = getGlobalConfig();
494
- if (globalConfig$1?.captureResponseBody !== void 0) return globalConfig$1.captureResponseBody;
495
- return false;
496
- }
497
- /**
498
- * Captures request body from string or Buffer data
499
- */
500
- function captureRequestBody(span, data, maxSize, semanticAttr, url) {
501
- if (!shouldCaptureRequestBody$1(url)) return;
502
- if (data.length && data.length <= maxSize) try {
503
- const requestBody = typeof data === "string" ? data : data.toString("utf-8");
920
+ function captureRequestBody(span, data, semanticAttr, url) {
921
+ if (!shouldCaptureRequestBody(url)) return;
922
+ if (data.length) try {
923
+ const requestBody = data.toString("utf-8");
504
924
  if (requestBody) setAttributeValue(span, semanticAttr, requestBody);
505
925
  } catch (e) {
506
926
  console.error("Error occurred while capturing request body:", e);
@@ -509,25 +929,13 @@ function captureRequestBody(span, data, maxSize, semanticAttr, url) {
509
929
  /**
510
930
  * Captures response body from chunks
511
931
  */
512
- function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url, maxSize) {
513
- if (!shouldCaptureResponseBody$1(url)) return;
514
- if (chunks === null) {
515
- const contentEncoding = responseHeaders?.["content-encoding"];
516
- const contentType = responseHeaders?.["content-type"];
517
- const toHeaderString = (value) => typeof value === "string" ? value : Array.isArray(value) ? value.join(", ") : "unknown";
518
- setAttributeValue(span, semanticAttr, `[truncated response body; exceeded maxResponseBodySize=${maxSize ?? DEFAULT_MAX_RESPONSE_BODY_SIZE$1}; content-type=${toHeaderString(contentType)}; content-encoding=${toHeaderString(contentEncoding)}]`);
519
- return;
520
- }
932
+ function captureResponseBody(span, chunks, semanticAttr, responseHeaders, url) {
933
+ if (!shouldCaptureResponseBody(url)) return;
521
934
  if (chunks.length) try {
522
- const concatedChunks = Buffer.concat(chunks);
523
- const contentEncoding = responseHeaders?.["content-encoding"];
524
- if (isCompressedContentEncoding(contentEncoding)) {
525
- setAttributeValue(span, semanticAttr, concatedChunks.toString("base64"));
526
- const encStr = typeof contentEncoding === "string" ? contentEncoding : Array.isArray(contentEncoding) ? contentEncoding.map(String).join(", ") : void 0;
527
- if (encStr) setAttributeValue(span, HTTP_RESPONSE_CONTENT_ENCODING, encStr);
528
- } else {
529
- const bodyStr = bufferToBodyString(concatedChunks);
530
- 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);
531
939
  }
532
940
  } catch (e) {
533
941
  console.error("Error occurred while capturing response body:", e);
@@ -604,6 +1012,7 @@ function extractClientRequestPath(request) {
604
1012
  }
605
1013
  const PingopsHttpSemanticAttributes = PingopsSemanticAttributes;
606
1014
  var PingopsHttpInstrumentation = class extends HttpInstrumentation {
1015
+ _llmStateByRequest = /* @__PURE__ */ new WeakMap();
607
1016
  constructor(config) {
608
1017
  super(config);
609
1018
  this._config = this._createConfig(config);
@@ -625,19 +1034,26 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
625
1034
  _createConfig(config) {
626
1035
  return {
627
1036
  ...config,
628
- requestHook: this._createRequestHook(config?.requestHook, config),
629
- responseHook: this._createResponseHook(config?.responseHook, config)
1037
+ requestHook: this._createRequestHook(config?.requestHook),
1038
+ responseHook: this._createResponseHook(config?.responseHook)
630
1039
  };
631
1040
  }
632
- _createRequestHook(originalRequestHook, config) {
1041
+ _createRequestHook(originalRequestHook) {
633
1042
  return (span, request) => {
634
1043
  const headers = extractRequestHeaders(request);
635
1044
  if (headers) captureRequestHeaders(span, headers);
636
1045
  if (request instanceof ClientRequest) {
637
- const maxRequestBodySize = config?.maxRequestBodySize || DEFAULT_MAX_REQUEST_BODY_SIZE$1;
1046
+ let requestBodySize = 0;
1047
+ const requestContentLength = parseContentLength(headers?.["content-length"]);
1048
+ span.setAttribute(PingopsSemanticAttributes.HTTP_REQUEST_BODY_SIZE, requestContentLength !== void 0 ? Math.max(requestBodySize, requestContentLength) : requestBodySize);
638
1049
  const hostHeader = request.getHeader("host");
639
1050
  const host = typeof hostHeader === "string" ? hostHeader : Array.isArray(hostHeader) ? hostHeader.join(",") : typeof hostHeader === "number" ? String(hostHeader) : void 0;
640
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
+ }
641
1057
  if (typeof request.path === "string" && request.path.length > 0) {
642
1058
  const { path, query } = parseRequestPathAndQuery(request.path);
643
1059
  span.setAttribute(ATTR_URL_PATH, path);
@@ -647,49 +1063,68 @@ var PingopsHttpInstrumentation = class extends HttpInstrumentation {
647
1063
  const originalWrite = request.write.bind(request);
648
1064
  const originalEnd = request.end.bind(request);
649
1065
  request.write = ((data, ...rest) => {
650
- if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url);
1066
+ const chunkBuffer = toBufferChunk(data);
1067
+ if (chunkBuffer) {
1068
+ requestBodySize += chunkBuffer.length;
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);
1072
+ }
651
1073
  return originalWrite(data, ...rest);
652
1074
  });
653
1075
  request.end = ((data, ...rest) => {
654
- if (typeof data === "string" || data instanceof Buffer) captureRequestBody(span, data, maxRequestBodySize, PingopsSemanticAttributes.HTTP_REQUEST_BODY, url);
1076
+ const chunkBuffer = toBufferChunk(data);
1077
+ if (chunkBuffer) {
1078
+ requestBodySize += chunkBuffer.length;
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);
1082
+ }
655
1083
  return originalEnd(data, ...rest);
656
1084
  });
657
1085
  }
658
1086
  if (originalRequestHook) originalRequestHook(span, request);
659
1087
  };
660
1088
  }
661
- _createResponseHook(originalResponseHook, config) {
1089
+ _createResponseHook(originalResponseHook) {
662
1090
  return (span, response) => {
663
1091
  const headers = extractResponseHeaders(response);
664
1092
  if (headers) captureResponseHeaders(span, headers);
665
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;
666
1096
  const requestPath = response.req instanceof ClientRequest ? extractClientRequestPath(response.req) : void 0;
667
1097
  if (requestPath) {
668
1098
  const { path, query } = parseRequestPathAndQuery(requestPath);
669
1099
  span.setAttribute(ATTR_URL_PATH, path);
670
1100
  if (query) span.setAttribute(ATTR_URL_QUERY, query);
671
1101
  }
672
- const maxResponseBodySize = config?.maxResponseBodySize || DEFAULT_MAX_RESPONSE_BODY_SIZE$1;
673
1102
  const url = response.url || void 0;
674
- let chunks = [];
1103
+ const chunks = [];
675
1104
  let totalSize = 0;
676
- const shouldCapture = shouldCaptureResponseBody$1(url);
1105
+ span.setAttribute(PingopsSemanticAttributes.HTTP_RESPONSE_BODY_SIZE, 0);
1106
+ const shouldCapture = shouldCaptureResponseBody(url);
1107
+ if (llmState && headers) updateLlmResponseHeaders(llmState, headers);
677
1108
  response.prependListener("data", (chunk) => {
678
- if (!chunk || !shouldCapture) return;
679
- let chunkBuffer = null;
680
- if (typeof chunk === "string") chunkBuffer = Buffer.from(chunk);
681
- else if (Buffer.isBuffer(chunk)) chunkBuffer = chunk;
682
- else if (chunk instanceof Uint8Array) chunkBuffer = Buffer.from(chunk);
1109
+ if (!chunk) return;
1110
+ const chunkBuffer = toBufferChunk(chunk);
683
1111
  if (!chunkBuffer) return;
684
1112
  totalSize += chunkBuffer.length;
685
- if (chunks && totalSize <= maxResponseBodySize) chunks.push(chunkBuffer);
686
- else chunks = null;
1113
+ span.setAttribute(PingopsSemanticAttributes.HTTP_RESPONSE_BODY_SIZE, totalSize);
1114
+ if (llmState) appendLlmResponseChunk(llmState, chunkBuffer);
1115
+ if (!shouldCapture) return;
1116
+ chunks.push(chunkBuffer);
687
1117
  });
688
1118
  let finalized = false;
689
1119
  const finalizeCapture = () => {
690
1120
  if (finalized) return;
691
1121
  finalized = true;
692
- captureResponseBody(span, chunks, PingopsSemanticAttributes.HTTP_RESPONSE_BODY, headers, url, maxResponseBodySize);
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);
693
1128
  };
694
1129
  response.prependOnceListener("end", finalizeCapture);
695
1130
  response.prependOnceListener("close", finalizeCapture);
@@ -721,7 +1156,6 @@ function toRequestUrl$1(request) {
721
1156
  * @returns PingopsHttpInstrumentation instance
722
1157
  */
723
1158
  function createHttpInstrumentation(config) {
724
- const globalConfig$1 = getGlobalConfig();
725
1159
  const userIgnoreOutgoingRequestHook = config?.ignoreOutgoingRequestHook;
726
1160
  return new PingopsHttpInstrumentation({
727
1161
  ...config,
@@ -729,67 +1163,12 @@ function createHttpInstrumentation(config) {
729
1163
  ignoreOutgoingRequestHook: (request) => {
730
1164
  if (shouldIgnoreOutboundInstrumentation(toRequestUrl$1(request))) return true;
731
1165
  return userIgnoreOutgoingRequestHook?.(request) ?? false;
732
- },
733
- maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
734
- maxResponseBodySize: globalConfig$1?.maxResponseBodySize
1166
+ }
735
1167
  });
736
1168
  }
737
1169
 
738
1170
  //#endregion
739
1171
  //#region src/instrumentations/undici/pingops-undici.ts
740
- const DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024;
741
- const DEFAULT_MAX_RESPONSE_BODY_SIZE = 4 * 1024;
742
- const HTTP_REQUEST_BODY = "http.request.body";
743
- const HTTP_RESPONSE_BODY = "http.response.body";
744
- /**
745
- * Extracts domain from URL
746
- */
747
- function extractDomainFromUrl(url) {
748
- try {
749
- return new URL$1(url).hostname;
750
- } catch {
751
- const match = url.match(/^(?:https?:\/\/)?([^/]+)/);
752
- return match ? match[1] : "";
753
- }
754
- }
755
- /**
756
- * Gets domain rule configuration for a given URL
757
- */
758
- function getDomainRule(url, domainAllowList) {
759
- if (!domainAllowList) return;
760
- const domain = extractDomainFromUrl(url);
761
- for (const rule of domainAllowList) if (domain === rule.domain || domain.endsWith(`.${rule.domain}`) || domain === rule.domain.slice(1)) return rule;
762
- }
763
- /**
764
- * Determines if request body should be captured based on priority:
765
- * context > domain rule > global config > default (false)
766
- */
767
- function shouldCaptureRequestBody(url) {
768
- const contextValue = context.active().getValue(PINGOPS_CAPTURE_REQUEST_BODY);
769
- if (contextValue !== void 0) return contextValue;
770
- if (url) {
771
- const domainRule = getDomainRule(url, getGlobalConfig()?.domainAllowList);
772
- if (domainRule?.captureRequestBody !== void 0) return domainRule.captureRequestBody;
773
- }
774
- const globalConfig$1 = getGlobalConfig();
775
- if (globalConfig$1?.captureRequestBody !== void 0) return globalConfig$1.captureRequestBody;
776
- return false;
777
- }
778
- /**
779
- * Determines if response body should be captured based on priority:
780
- * context > domain rule > global config > default (false)
781
- */
782
- function shouldCaptureResponseBody(url) {
783
- const contextValue = context.active().getValue(PINGOPS_CAPTURE_RESPONSE_BODY);
784
- if (contextValue !== void 0) return contextValue;
785
- if (url) {
786
- const domainRule = getDomainRule(url, getGlobalConfig()?.domainAllowList);
787
- if (domainRule?.captureResponseBody !== void 0) return domainRule.captureResponseBody;
788
- }
789
- const globalConfig$1 = getGlobalConfig();
790
- if (globalConfig$1?.captureResponseBody !== void 0) return globalConfig$1.captureResponseBody;
791
- return false;
792
- }
793
1172
  var UndiciInstrumentation = class extends InstrumentationBase {
794
1173
  _recordFromReq = /* @__PURE__ */ new WeakMap();
795
1174
  constructor(config = {}) {
@@ -897,7 +1276,9 @@ var UndiciInstrumentation = class extends InstrumentationBase {
897
1276
  [ATTR_URL_FULL]: requestUrl.toString(),
898
1277
  [ATTR_URL_PATH]: requestUrl.pathname,
899
1278
  [ATTR_URL_QUERY]: requestUrl.search,
900
- [ATTR_URL_SCHEME]: urlScheme
1279
+ [ATTR_URL_SCHEME]: urlScheme,
1280
+ [HTTP_REQUEST_BODY_SIZE]: 0,
1281
+ [HTTP_RESPONSE_BODY_SIZE]: 0
901
1282
  };
902
1283
  const schemePorts = {
903
1284
  https: "443",
@@ -907,7 +1288,10 @@ var UndiciInstrumentation = class extends InstrumentationBase {
907
1288
  const serverPort = requestUrl.port || schemePorts[urlScheme];
908
1289
  attributes[ATTR_SERVER_ADDRESS] = serverAddress;
909
1290
  if (serverPort && !isNaN(Number(serverPort))) attributes[ATTR_SERVER_PORT] = Number(serverPort);
910
- const userAgentValues = this.parseRequestHeaders(request).get("user-agent");
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");
911
1295
  if (userAgentValues) attributes[ATTR_USER_AGENT_ORIGINAL] = Array.isArray(userAgentValues) ? userAgentValues[userAgentValues.length - 1] : userAgentValues;
912
1296
  const hookAttributes = safeExecuteInTheMiddle(() => config.startSpanHook?.(request), (e) => e && this._diag.error("caught startSpanHook error: ", e), true);
913
1297
  if (hookAttributes) Object.entries(hookAttributes).forEach(([key, val]) => {
@@ -940,13 +1324,16 @@ var UndiciInstrumentation = class extends InstrumentationBase {
940
1324
  responseBodyChunks: [],
941
1325
  requestBodySize: 0,
942
1326
  responseBodySize: 0,
943
- url: requestUrl.toString()
1327
+ url: requestUrl.toString(),
1328
+ llmState: createLlmEnrichmentState(span, requestUrl.toString(), request.method, requestHeadersObject)
944
1329
  });
1330
+ const createdRecord = this._recordFromReq.get(request);
1331
+ if (createdRecord?.llmState) updateLlmRequestHeaders(createdRecord.llmState, requestHeadersObject);
945
1332
  }
946
1333
  onRequestHeaders({ request, socket }) {
947
1334
  const record = this._recordFromReq.get(request);
948
1335
  if (!record) return;
949
- const { span } = record;
1336
+ const { span, attributes } = record;
950
1337
  const spanAttributes = {};
951
1338
  const remoteAddress = typeof socket.remoteAddress === "string" ? socket.remoteAddress : void 0;
952
1339
  const remotePort = typeof socket.remotePort === "number" ? socket.remotePort : void 0;
@@ -958,12 +1345,19 @@ var UndiciInstrumentation = class extends InstrumentationBase {
958
1345
  spanAttributes[`http.request.header.${name}`] = attrValue;
959
1346
  }
960
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
+ }
961
1354
  }
962
1355
  onResponseHeaders({ request, response }) {
963
1356
  const record = this._recordFromReq.get(request);
964
1357
  if (!record) return;
965
1358
  const { span, attributes } = record;
966
1359
  const spanAttributes = { [ATTR_HTTP_RESPONSE_STATUS_CODE]: response.statusCode };
1360
+ const responseHeadersObject = {};
967
1361
  const config = this.getConfig();
968
1362
  safeExecuteInTheMiddle(() => config.responseHook?.(span, {
969
1363
  request,
@@ -973,6 +1367,7 @@ var UndiciInstrumentation = class extends InstrumentationBase {
973
1367
  const name = response.headers[idx].toString().toLowerCase();
974
1368
  const value = response.headers[idx + 1];
975
1369
  spanAttributes[`http.response.header.${name}`] = value.toString();
1370
+ responseHeadersObject[name] = value.toString();
976
1371
  if (name === "content-length") {
977
1372
  const contentLength = Number(value.toString());
978
1373
  if (!isNaN(contentLength)) spanAttributes["http.response.header.content-length"] = contentLength;
@@ -981,29 +1376,33 @@ var UndiciInstrumentation = class extends InstrumentationBase {
981
1376
  span.setAttributes(spanAttributes);
982
1377
  span.setStatus({ code: response.statusCode >= 400 ? SpanStatusCode.ERROR : SpanStatusCode.UNSET });
983
1378
  record.attributes = Object.assign(attributes, spanAttributes);
1379
+ if (record.llmState) updateLlmResponseHeaders(record.llmState, responseHeadersObject);
984
1380
  }
985
1381
  onDone({ request }) {
986
1382
  const record = this._recordFromReq.get(request);
987
1383
  if (!record) return;
988
1384
  const { span, attributes, startTime } = record;
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);
989
1391
  if (shouldCaptureResponseBody(record.url)) {
990
- const maxResponseBodySize = this.getConfig().maxResponseBodySize ?? DEFAULT_MAX_RESPONSE_BODY_SIZE;
991
- const contentEncoding = record.attributes?.["http.response.header.content-encoding"] ?? void 0;
992
- const contentType = record.attributes?.["http.response.header.content-type"] ?? void 0;
993
- if (record.responseBodySize === Infinity) span.setAttribute(HTTP_RESPONSE_BODY, `[truncated response body; exceeded maxResponseBodySize=${maxResponseBodySize}; content-type=${contentType ?? "unknown"}; content-encoding=${contentEncoding ?? "identity"}]`);
994
- else if (record.responseBodyChunks.length > 0) try {
995
- const responseBodyBuffer = Buffer.concat(record.responseBodyChunks);
996
- if (isCompressedContentEncoding(contentEncoding)) {
997
- span.setAttribute(HTTP_RESPONSE_BODY, responseBodyBuffer.toString("base64"));
998
- if (contentEncoding) span.setAttribute(HTTP_RESPONSE_CONTENT_ENCODING, contentEncoding);
999
- } else {
1000
- const bodyStr = bufferToBodyString(responseBodyBuffer);
1001
- 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);
1002
1400
  }
1003
1401
  } catch (e) {
1004
1402
  this._diag.error("Error occurred while capturing response body:", e);
1005
1403
  }
1006
1404
  }
1405
+ if (record.llmState) finalizeLlmEnrichment(record.llmState);
1007
1406
  span.end();
1008
1407
  this._recordFromReq.delete(request);
1009
1408
  this.recordRequestDuration(attributes, startTime);
@@ -1012,10 +1411,19 @@ var UndiciInstrumentation = class extends InstrumentationBase {
1012
1411
  const record = this._recordFromReq.get(request);
1013
1412
  if (!record) return;
1014
1413
  const { span, attributes, startTime } = record;
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);
1015
1420
  if (shouldCaptureRequestBody(record.url)) {
1016
- if (record.requestBodyChunks.length > 0 && record.requestBodySize !== Infinity) try {
1017
- const requestBody = Buffer.concat(record.requestBodyChunks).toString("utf-8");
1018
- if (requestBody) span.setAttribute(HTTP_REQUEST_BODY, requestBody);
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);
1019
1427
  } catch (e) {
1020
1428
  this._diag.error("Error occurred while capturing request body:", e);
1021
1429
  }
@@ -1026,6 +1434,7 @@ var UndiciInstrumentation = class extends InstrumentationBase {
1026
1434
  code: SpanStatusCode.ERROR,
1027
1435
  message: errorMessage
1028
1436
  });
1437
+ if (record.llmState) finalizeLlmEnrichment(record.llmState);
1029
1438
  span.end();
1030
1439
  this._recordFromReq.delete(request);
1031
1440
  attributes[ATTR_ERROR_TYPE] = errorMessage;
@@ -1034,29 +1443,27 @@ var UndiciInstrumentation = class extends InstrumentationBase {
1034
1443
  onBodyChunkSent({ request, chunk }) {
1035
1444
  const record = this._recordFromReq.get(request);
1036
1445
  if (!record) return;
1446
+ record.requestBodySize += chunk.length;
1447
+ if (record.llmState) appendLlmRequestChunk(record.llmState, chunk);
1037
1448
  if (!shouldCaptureRequestBody(record.url)) return;
1038
- const maxRequestBodySize = this.getConfig().maxRequestBodySize ?? DEFAULT_MAX_REQUEST_BODY_SIZE;
1039
- if (record.requestBodySize + chunk.length <= maxRequestBodySize) {
1040
- record.requestBodyChunks.push(chunk);
1041
- record.requestBodySize += chunk.length;
1042
- } else {
1043
- record.requestBodySize = Infinity;
1044
- record.requestBodyChunks = [];
1045
- }
1449
+ record.requestBodyChunks.push(chunk);
1046
1450
  }
1047
1451
  onBodySent({ request }) {
1048
1452
  const record = this._recordFromReq.get(request);
1049
1453
  if (!record) return;
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);
1050
1457
  if (!shouldCaptureRequestBody(record.url)) {
1051
1458
  record.requestBodyChunks = [];
1052
1459
  return;
1053
1460
  }
1054
- if (record.requestBodySize === Infinity) {
1055
- const maxRequestBodySize = this.getConfig().maxRequestBodySize ?? DEFAULT_MAX_REQUEST_BODY_SIZE;
1056
- record.span.setAttribute(HTTP_REQUEST_BODY, `[truncated request body; exceeded maxRequestBodySize=${maxRequestBodySize}]`);
1057
- } else if (record.requestBodyChunks.length > 0) try {
1058
- const requestBody = Buffer.concat(record.requestBodyChunks).toString("utf-8");
1059
- if (requestBody) record.span.setAttribute(HTTP_REQUEST_BODY, requestBody);
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);
1060
1467
  } catch (e) {
1061
1468
  this._diag.error("Error occurred while capturing request body:", e);
1062
1469
  }
@@ -1065,15 +1472,10 @@ var UndiciInstrumentation = class extends InstrumentationBase {
1065
1472
  onBodyChunkReceived({ request, chunk }) {
1066
1473
  const record = this._recordFromReq.get(request);
1067
1474
  if (!record) return;
1475
+ record.responseBodySize += chunk.length;
1476
+ if (record.llmState) appendLlmResponseChunk(record.llmState, chunk);
1068
1477
  if (!shouldCaptureResponseBody(record.url)) return;
1069
- const maxResponseBodySize = this.getConfig().maxResponseBodySize ?? DEFAULT_MAX_RESPONSE_BODY_SIZE;
1070
- if (record.responseBodySize + chunk.length <= maxResponseBodySize) {
1071
- record.responseBodyChunks.push(chunk);
1072
- record.responseBodySize += chunk.length;
1073
- } else {
1074
- record.responseBodySize = Infinity;
1075
- record.responseBodyChunks = [];
1076
- }
1478
+ record.responseBodyChunks.push(chunk);
1077
1479
  }
1078
1480
  recordRequestDuration(attributes, startTime) {
1079
1481
  const metricsAttributes = {};
@@ -1125,14 +1527,11 @@ function toRequestUrl(request) {
1125
1527
  * @returns UndiciInstrumentation instance
1126
1528
  */
1127
1529
  function createUndiciInstrumentation() {
1128
- const globalConfig$1 = getGlobalConfig();
1129
1530
  return new UndiciInstrumentation({
1130
1531
  enabled: true,
1131
1532
  ignoreRequestHook: (request) => {
1132
1533
  return shouldIgnoreOutboundInstrumentation(toRequestUrl(request));
1133
- },
1134
- maxRequestBodySize: globalConfig$1?.maxRequestBodySize,
1135
- maxResponseBodySize: globalConfig$1?.maxResponseBodySize
1534
+ }
1136
1535
  });
1137
1536
  }
1138
1537