@pingops/otel 0.3.0 → 0.4.0

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