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