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