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