observability-toolkit 1.8.5 → 2.0.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 +126 -5
- package/dist/backends/index.d.ts +163 -0
- package/dist/backends/index.d.ts.map +1 -1
- package/dist/backends/index.js +57 -0
- package/dist/backends/index.js.map +1 -1
- package/dist/backends/index.test.js +55 -1
- package/dist/backends/index.test.js.map +1 -1
- package/dist/backends/local-jsonl.d.ts +30 -0
- package/dist/backends/local-jsonl.d.ts.map +1 -1
- package/dist/backends/local-jsonl.js +912 -550
- package/dist/backends/local-jsonl.js.map +1 -1
- package/dist/backends/signoz-api-rate-limiter.test.js +2 -1
- package/dist/backends/signoz-api-rate-limiter.test.js.map +1 -1
- package/dist/backends/signoz-api.d.ts +16 -2
- package/dist/backends/signoz-api.d.ts.map +1 -1
- package/dist/backends/signoz-api.js +650 -534
- package/dist/backends/signoz-api.js.map +1 -1
- package/dist/backends/signoz-api.test.js +6 -5
- package/dist/backends/signoz-api.test.js.map +1 -1
- package/dist/lib/agent-as-judge.d.ts +388 -0
- package/dist/lib/agent-as-judge.d.ts.map +1 -0
- package/dist/lib/agent-as-judge.js +740 -0
- package/dist/lib/agent-as-judge.js.map +1 -0
- package/dist/lib/agent-as-judge.test.d.ts +5 -0
- package/dist/lib/agent-as-judge.test.d.ts.map +1 -0
- package/dist/lib/agent-as-judge.test.js +816 -0
- package/dist/lib/agent-as-judge.test.js.map +1 -0
- package/dist/lib/cache.d.ts +15 -2
- package/dist/lib/cache.d.ts.map +1 -1
- package/dist/lib/cache.js +16 -2
- package/dist/lib/cache.js.map +1 -1
- package/dist/lib/circuit-breaker.d.ts +18 -0
- package/dist/lib/circuit-breaker.d.ts.map +1 -1
- package/dist/lib/circuit-breaker.js +41 -8
- package/dist/lib/circuit-breaker.js.map +1 -1
- package/dist/lib/confident-export.d.ts +101 -0
- package/dist/lib/confident-export.d.ts.map +1 -0
- package/dist/lib/confident-export.js +393 -0
- package/dist/lib/confident-export.js.map +1 -0
- package/dist/lib/confident-export.test.d.ts +7 -0
- package/dist/lib/confident-export.test.d.ts.map +1 -0
- package/dist/lib/confident-export.test.js +835 -0
- package/dist/lib/confident-export.test.js.map +1 -0
- package/dist/lib/constants.d.ts +75 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +104 -1
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/datadog-export.d.ts +156 -0
- package/dist/lib/datadog-export.d.ts.map +1 -0
- package/dist/lib/datadog-export.js +464 -0
- package/dist/lib/datadog-export.js.map +1 -0
- package/dist/lib/datadog-export.test.d.ts +14 -0
- package/dist/lib/datadog-export.test.d.ts.map +1 -0
- package/dist/lib/datadog-export.test.js +890 -0
- package/dist/lib/datadog-export.test.js.map +1 -0
- package/dist/lib/evaluation-hooks.d.ts +49 -0
- package/dist/lib/evaluation-hooks.d.ts.map +1 -0
- package/dist/lib/evaluation-hooks.js +488 -0
- package/dist/lib/evaluation-hooks.js.map +1 -0
- package/dist/lib/evaluation-hooks.test.d.ts +8 -0
- package/dist/lib/evaluation-hooks.test.d.ts.map +1 -0
- package/dist/lib/evaluation-hooks.test.js +624 -0
- package/dist/lib/evaluation-hooks.test.js.map +1 -0
- package/dist/lib/export-utils.d.ts +99 -0
- package/dist/lib/export-utils.d.ts.map +1 -0
- package/dist/lib/export-utils.js +238 -0
- package/dist/lib/export-utils.js.map +1 -0
- package/dist/lib/export-utils.test.d.ts +5 -0
- package/dist/lib/export-utils.test.d.ts.map +1 -0
- package/dist/lib/export-utils.test.js +193 -0
- package/dist/lib/export-utils.test.js.map +1 -0
- package/dist/lib/file-utils.d.ts +17 -2
- package/dist/lib/file-utils.d.ts.map +1 -1
- package/dist/lib/file-utils.js +24 -5
- package/dist/lib/file-utils.js.map +1 -1
- package/dist/lib/file-utils.test.js +30 -0
- package/dist/lib/file-utils.test.js.map +1 -1
- package/dist/lib/histogram.d.ts +119 -0
- package/dist/lib/histogram.d.ts.map +1 -0
- package/dist/lib/histogram.js +202 -0
- package/dist/lib/histogram.js.map +1 -0
- package/dist/lib/histogram.test.d.ts +5 -0
- package/dist/lib/histogram.test.d.ts.map +1 -0
- package/dist/lib/histogram.test.js +381 -0
- package/dist/lib/histogram.test.js.map +1 -0
- package/dist/lib/instrumentation.d.ts +153 -0
- package/dist/lib/instrumentation.d.ts.map +1 -0
- package/dist/lib/instrumentation.integration.test.d.ts +2 -0
- package/dist/lib/instrumentation.integration.test.d.ts.map +1 -0
- package/dist/lib/instrumentation.integration.test.js +589 -0
- package/dist/lib/instrumentation.integration.test.js.map +1 -0
- package/dist/lib/instrumentation.js +520 -0
- package/dist/lib/instrumentation.js.map +1 -0
- package/dist/lib/instrumentation.test.d.ts +2 -0
- package/dist/lib/instrumentation.test.d.ts.map +1 -0
- package/dist/lib/instrumentation.test.js +821 -0
- package/dist/lib/instrumentation.test.js.map +1 -0
- package/dist/lib/langfuse-export.d.ts +125 -0
- package/dist/lib/langfuse-export.d.ts.map +1 -0
- package/dist/lib/langfuse-export.js +367 -0
- package/dist/lib/langfuse-export.js.map +1 -0
- package/dist/lib/langfuse-export.test.d.ts +7 -0
- package/dist/lib/langfuse-export.test.d.ts.map +1 -0
- package/dist/lib/langfuse-export.test.js +1007 -0
- package/dist/lib/langfuse-export.test.js.map +1 -0
- package/dist/lib/llm-as-judge.d.ts +657 -0
- package/dist/lib/llm-as-judge.d.ts.map +1 -0
- package/dist/lib/llm-as-judge.js +1397 -0
- package/dist/lib/llm-as-judge.js.map +1 -0
- package/dist/lib/llm-as-judge.test.d.ts +2 -0
- package/dist/lib/llm-as-judge.test.d.ts.map +1 -0
- package/dist/lib/llm-as-judge.test.js +2409 -0
- package/dist/lib/llm-as-judge.test.js.map +1 -0
- package/dist/lib/logger.d.ts +1 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/metrics.d.ts +62 -0
- package/dist/lib/metrics.d.ts.map +1 -0
- package/dist/lib/metrics.js +166 -0
- package/dist/lib/metrics.js.map +1 -0
- package/dist/lib/metrics.test.d.ts +5 -0
- package/dist/lib/metrics.test.d.ts.map +1 -0
- package/dist/lib/metrics.test.js +189 -0
- package/dist/lib/metrics.test.js.map +1 -0
- package/dist/lib/parse-stats.d.ts +119 -0
- package/dist/lib/parse-stats.d.ts.map +1 -0
- package/dist/lib/parse-stats.js +206 -0
- package/dist/lib/parse-stats.js.map +1 -0
- package/dist/lib/parse-stats.test.d.ts +5 -0
- package/dist/lib/parse-stats.test.d.ts.map +1 -0
- package/dist/lib/parse-stats.test.js +283 -0
- package/dist/lib/parse-stats.test.js.map +1 -0
- package/dist/lib/phoenix-export.d.ts +109 -0
- package/dist/lib/phoenix-export.d.ts.map +1 -0
- package/dist/lib/phoenix-export.js +429 -0
- package/dist/lib/phoenix-export.js.map +1 -0
- package/dist/lib/phoenix-export.test.d.ts +11 -0
- package/dist/lib/phoenix-export.test.d.ts.map +1 -0
- package/dist/lib/phoenix-export.test.js +725 -0
- package/dist/lib/phoenix-export.test.js.map +1 -0
- package/dist/lib/server-utils.d.ts +6 -1
- package/dist/lib/server-utils.d.ts.map +1 -1
- package/dist/lib/server-utils.js +9 -1
- package/dist/lib/server-utils.js.map +1 -1
- package/dist/lib/shared-schemas.d.ts +6 -0
- package/dist/lib/shared-schemas.d.ts.map +1 -1
- package/dist/lib/shared-schemas.js +11 -4
- package/dist/lib/shared-schemas.js.map +1 -1
- package/dist/lib/verification-events.d.ts +100 -0
- package/dist/lib/verification-events.d.ts.map +1 -0
- package/dist/lib/verification-events.js +162 -0
- package/dist/lib/verification-events.js.map +1 -0
- package/dist/lib/verification-events.test.d.ts +5 -0
- package/dist/lib/verification-events.test.d.ts.map +1 -0
- package/dist/lib/verification-events.test.js +193 -0
- package/dist/lib/verification-events.test.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +77 -21
- package/dist/server.js.map +1 -1
- package/dist/tools/context-stats.d.ts.map +1 -1
- package/dist/tools/context-stats.js +6 -8
- package/dist/tools/context-stats.js.map +1 -1
- package/dist/tools/export-confident.d.ts +145 -0
- package/dist/tools/export-confident.d.ts.map +1 -0
- package/dist/tools/export-confident.js +134 -0
- package/dist/tools/export-confident.js.map +1 -0
- package/dist/tools/export-confident.test.d.ts +7 -0
- package/dist/tools/export-confident.test.d.ts.map +1 -0
- package/dist/tools/export-confident.test.js +332 -0
- package/dist/tools/export-confident.test.js.map +1 -0
- package/dist/tools/export-datadog.d.ts +160 -0
- package/dist/tools/export-datadog.d.ts.map +1 -0
- package/dist/tools/export-datadog.js +160 -0
- package/dist/tools/export-datadog.js.map +1 -0
- package/dist/tools/export-datadog.test.d.ts +8 -0
- package/dist/tools/export-datadog.test.d.ts.map +1 -0
- package/dist/tools/export-datadog.test.js +419 -0
- package/dist/tools/export-datadog.test.js.map +1 -0
- package/dist/tools/export-langfuse.d.ts +137 -0
- package/dist/tools/export-langfuse.d.ts.map +1 -0
- package/dist/tools/export-langfuse.js +131 -0
- package/dist/tools/export-langfuse.js.map +1 -0
- package/dist/tools/export-langfuse.test.d.ts +7 -0
- package/dist/tools/export-langfuse.test.d.ts.map +1 -0
- package/dist/tools/export-langfuse.test.js +303 -0
- package/dist/tools/export-langfuse.test.js.map +1 -0
- package/dist/tools/export-phoenix.d.ts +145 -0
- package/dist/tools/export-phoenix.d.ts.map +1 -0
- package/dist/tools/export-phoenix.js +135 -0
- package/dist/tools/export-phoenix.js.map +1 -0
- package/dist/tools/export-phoenix.test.d.ts +7 -0
- package/dist/tools/export-phoenix.test.d.ts.map +1 -0
- package/dist/tools/export-phoenix.test.js +316 -0
- package/dist/tools/export-phoenix.test.js.map +1 -0
- package/dist/tools/health-check.d.ts +26 -0
- package/dist/tools/health-check.d.ts.map +1 -1
- package/dist/tools/health-check.js +36 -7
- package/dist/tools/health-check.js.map +1 -1
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inject-evaluations.d.ts +1315 -0
- package/dist/tools/inject-evaluations.d.ts.map +1 -0
- package/dist/tools/inject-evaluations.js +121 -0
- package/dist/tools/inject-evaluations.js.map +1 -0
- package/dist/tools/inject-evaluations.test.d.ts +5 -0
- package/dist/tools/inject-evaluations.test.d.ts.map +1 -0
- package/dist/tools/inject-evaluations.test.js +359 -0
- package/dist/tools/inject-evaluations.test.js.map +1 -0
- package/dist/tools/query-evaluations.d.ts +25 -4
- package/dist/tools/query-evaluations.d.ts.map +1 -1
- package/dist/tools/query-evaluations.js +10 -0
- package/dist/tools/query-evaluations.js.map +1 -1
- package/dist/tools/query-llm-events.js +2 -2
- package/dist/tools/query-llm-events.js.map +1 -1
- package/dist/tools/query-logs.d.ts +8 -8
- package/dist/tools/query-logs.js +3 -3
- package/dist/tools/query-logs.js.map +1 -1
- package/dist/tools/query-metrics.d.ts +4 -4
- package/dist/tools/query-metrics.js +2 -2
- package/dist/tools/query-metrics.js.map +1 -1
- package/dist/tools/query-traces.d.ts +8 -8
- package/dist/tools/query-verifications.d.ts +111 -0
- package/dist/tools/query-verifications.d.ts.map +1 -0
- package/dist/tools/query-verifications.js +101 -0
- package/dist/tools/query-verifications.js.map +1 -0
- package/dist/tools/query-verifications.test.d.ts +5 -0
- package/dist/tools/query-verifications.test.d.ts.map +1 -0
- package/dist/tools/query-verifications.test.js +156 -0
- package/dist/tools/query-verifications.test.js.map +1 -0
- package/dist/types/evaluation-hooks.d.ts +176 -0
- package/dist/types/evaluation-hooks.d.ts.map +1 -0
- package/dist/types/evaluation-hooks.js +49 -0
- package/dist/types/evaluation-hooks.js.map +1 -0
- package/package.json +10 -2
|
@@ -5,15 +5,19 @@
|
|
|
5
5
|
* span or log record, not the batched OpenTelemetry export format.
|
|
6
6
|
*/
|
|
7
7
|
import { join } from 'path';
|
|
8
|
-
import { GENAI_EVALUATION_ATTRIBUTES, } from './index.js';
|
|
8
|
+
import { GENAI_EVALUATION_ATTRIBUTES, GENAI_AGENT_ATTRIBUTES, AGENT_JUDGE_ATTRIBUTES, } from './index.js';
|
|
9
9
|
import { convertToOTLPTraces, convertToOTLPLogs, convertToOTLPMetrics, } from '../lib/otlp-export.js';
|
|
10
|
-
import { TELEMETRY_DIR, getTelemetryDirectories, getSpanKind, getStatusCodeName } from '../lib/constants.js';
|
|
10
|
+
import { TELEMETRY_DIR, getTelemetryDirectories, getSpanKind, getStatusCodeName, WORST_FILES_LIMIT } from '../lib/constants.js';
|
|
11
11
|
import { listFiles, streamJsonl, parseDateFromFilename, getDateString, paginateResults, hasReachedLimit, } from '../lib/file-utils.js';
|
|
12
12
|
import { QueryCache, makeCacheKey } from '../lib/cache.js';
|
|
13
|
+
import { recordQueryDuration, recordCacheHit } from '../lib/metrics.js';
|
|
14
|
+
import { Histogram } from '../lib/histogram.js';
|
|
13
15
|
import { getIndexPath, readIndex, isIndexStale, queryIndex, readLinesByNumber, } from '../lib/indexer.js';
|
|
14
16
|
import { sanitizePath } from '../lib/error-sanitizer.js';
|
|
15
17
|
import { CircuitBreaker } from '../lib/circuit-breaker.js';
|
|
18
|
+
import { withSpan } from '../lib/instrumentation.js';
|
|
16
19
|
import { existsSync } from 'fs';
|
|
20
|
+
import { ParseStatsTracker } from '../lib/parse-stats.js';
|
|
17
21
|
function startTiming() {
|
|
18
22
|
const start = performance.now();
|
|
19
23
|
return {
|
|
@@ -49,7 +53,9 @@ function getCachedRegex(pattern) {
|
|
|
49
53
|
return regex;
|
|
50
54
|
}
|
|
51
55
|
catch {
|
|
52
|
-
|
|
56
|
+
// Security: Truncate pattern in log to prevent log injection/info disclosure
|
|
57
|
+
const truncatedPattern = pattern.length > 50 ? pattern.slice(0, 50) + '...' : pattern;
|
|
58
|
+
console.warn(`Invalid regex pattern: ${truncatedPattern.replace(/[\n\r]/g, ' ')}`);
|
|
53
59
|
return null;
|
|
54
60
|
}
|
|
55
61
|
}
|
|
@@ -216,6 +222,82 @@ function extractValidEvaluatorType(value) {
|
|
|
216
222
|
* Convert flat evaluation to normalized EvaluationResult
|
|
217
223
|
* Includes runtime validation for data integrity (P0-2 fix)
|
|
218
224
|
*/
|
|
225
|
+
/**
|
|
226
|
+
* Safely extract StepScore array from raw attribute value.
|
|
227
|
+
* Returns undefined if not a valid array or if parsing fails.
|
|
228
|
+
*/
|
|
229
|
+
function extractStepScores(value) {
|
|
230
|
+
if (!value)
|
|
231
|
+
return undefined;
|
|
232
|
+
// If it's a string, try to parse as JSON
|
|
233
|
+
let arr;
|
|
234
|
+
if (typeof value === 'string') {
|
|
235
|
+
try {
|
|
236
|
+
arr = JSON.parse(value);
|
|
237
|
+
if (!Array.isArray(arr))
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else if (Array.isArray(value)) {
|
|
245
|
+
arr = value;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
// Validate each element and filter invalid entries
|
|
251
|
+
return arr
|
|
252
|
+
.filter((item) => {
|
|
253
|
+
if (!item || typeof item !== 'object')
|
|
254
|
+
return false;
|
|
255
|
+
const obj = item;
|
|
256
|
+
return ((typeof obj.step === 'string' || typeof obj.step === 'number') &&
|
|
257
|
+
typeof obj.score === 'number' &&
|
|
258
|
+
Number.isFinite(obj.score));
|
|
259
|
+
})
|
|
260
|
+
.slice(0, 1000); // Respect MAX_STEP_SCORES limit
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Safely extract ToolVerification array from raw attribute value.
|
|
264
|
+
* Returns undefined if not a valid array or if parsing fails.
|
|
265
|
+
*/
|
|
266
|
+
function extractToolVerifications(value) {
|
|
267
|
+
if (!value)
|
|
268
|
+
return undefined;
|
|
269
|
+
// If it's a string, try to parse as JSON
|
|
270
|
+
let arr;
|
|
271
|
+
if (typeof value === 'string') {
|
|
272
|
+
try {
|
|
273
|
+
arr = JSON.parse(value);
|
|
274
|
+
if (!Array.isArray(arr))
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (Array.isArray(value)) {
|
|
282
|
+
arr = value;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
// Validate each element and filter invalid entries
|
|
288
|
+
return arr
|
|
289
|
+
.filter((item) => {
|
|
290
|
+
if (!item || typeof item !== 'object')
|
|
291
|
+
return false;
|
|
292
|
+
const obj = item;
|
|
293
|
+
return (typeof obj.toolName === 'string' &&
|
|
294
|
+
typeof obj.toolCorrect === 'boolean' &&
|
|
295
|
+
typeof obj.argsCorrect === 'boolean' &&
|
|
296
|
+
typeof obj.score === 'number' &&
|
|
297
|
+
Number.isFinite(obj.score));
|
|
298
|
+
})
|
|
299
|
+
.slice(0, 500); // Respect MAX_TOOL_VERIFICATIONS limit
|
|
300
|
+
}
|
|
219
301
|
function normalizeEvaluation(raw) {
|
|
220
302
|
const attrs = raw.attributes;
|
|
221
303
|
// evaluationName is required per OTel spec - must be non-empty string
|
|
@@ -239,6 +321,12 @@ function normalizeEvaluation(raw) {
|
|
|
239
321
|
traceId: raw.traceId,
|
|
240
322
|
spanId: raw.spanId,
|
|
241
323
|
sessionId: extractValidString(attrs?.['session.id']),
|
|
324
|
+
// Agent-as-Judge fields (Section 10.7)
|
|
325
|
+
agentId: extractValidString(attrs?.[GENAI_AGENT_ATTRIBUTES.AGENT_ID]),
|
|
326
|
+
agentName: extractValidString(attrs?.[GENAI_AGENT_ATTRIBUTES.AGENT_NAME]),
|
|
327
|
+
stepScores: extractStepScores(attrs?.[AGENT_JUDGE_ATTRIBUTES.STEP_SCORES]),
|
|
328
|
+
toolVerifications: extractToolVerifications(attrs?.[AGENT_JUDGE_ATTRIBUTES.TOOL_VERIFICATIONS]),
|
|
329
|
+
trajectoryLength: extractValidNumber(attrs?.[AGENT_JUDGE_ATTRIBUTES.TRAJECTORY_LENGTH]),
|
|
242
330
|
};
|
|
243
331
|
}
|
|
244
332
|
/**
|
|
@@ -460,17 +548,27 @@ function getFilesInRange(dir, pattern, startDate, endDate) {
|
|
|
460
548
|
export class LocalJsonlBackend {
|
|
461
549
|
name = 'local-jsonl';
|
|
462
550
|
telemetryDir;
|
|
463
|
-
traceCache = new QueryCache();
|
|
464
|
-
logCache = new QueryCache();
|
|
465
|
-
metricCache = new QueryCache();
|
|
466
|
-
llmEventCache = new QueryCache();
|
|
467
|
-
evaluationCache = new QueryCache();
|
|
551
|
+
traceCache = new QueryCache(100, 60000, 'traces');
|
|
552
|
+
logCache = new QueryCache(100, 60000, 'logs');
|
|
553
|
+
metricCache = new QueryCache(100, 60000, 'metrics');
|
|
554
|
+
llmEventCache = new QueryCache(100, 60000, 'llmEvents');
|
|
555
|
+
evaluationCache = new QueryCache(100, 60000, 'evaluations');
|
|
468
556
|
useIndexes;
|
|
469
557
|
circuitBreaker;
|
|
558
|
+
parseStatsTracker;
|
|
559
|
+
/** Histograms for tracking query latency per type */
|
|
560
|
+
queryHistograms = {
|
|
561
|
+
traces: new Histogram(),
|
|
562
|
+
logs: new Histogram(),
|
|
563
|
+
metrics: new Histogram(),
|
|
564
|
+
llmEvents: new Histogram(),
|
|
565
|
+
evaluations: new Histogram(),
|
|
566
|
+
};
|
|
470
567
|
constructor(telemetryDir, useIndexes = true) {
|
|
471
568
|
this.telemetryDir = telemetryDir || TELEMETRY_DIR;
|
|
472
569
|
this.useIndexes = useIndexes;
|
|
473
570
|
this.circuitBreaker = new CircuitBreaker({ name: 'local-file-io' });
|
|
571
|
+
this.parseStatsTracker = new ParseStatsTracker();
|
|
474
572
|
}
|
|
475
573
|
/**
|
|
476
574
|
* Get circuit breaker state (for health check and testing)
|
|
@@ -506,6 +604,41 @@ export class LocalJsonlBackend {
|
|
|
506
604
|
evaluations: this.evaluationCache.getStats(),
|
|
507
605
|
};
|
|
508
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* Get parse statistics for JSONL file processing.
|
|
609
|
+
* Returns aggregate stats including success rate and worst files.
|
|
610
|
+
*/
|
|
611
|
+
getParseStats() {
|
|
612
|
+
return this.parseStatsTracker.getAggregateStats();
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Reset parse statistics (useful for testing)
|
|
616
|
+
*/
|
|
617
|
+
resetParseStats() {
|
|
618
|
+
this.parseStatsTracker.reset();
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Get query latency statistics per query type
|
|
622
|
+
*/
|
|
623
|
+
getQueryStats() {
|
|
624
|
+
return {
|
|
625
|
+
traces: this.queryHistograms.traces.getStats(),
|
|
626
|
+
logs: this.queryHistograms.logs.getStats(),
|
|
627
|
+
metrics: this.queryHistograms.metrics.getStats(),
|
|
628
|
+
llmEvents: this.queryHistograms.llmEvents.getStats(),
|
|
629
|
+
evaluations: this.queryHistograms.evaluations.getStats(),
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Reset query latency histograms (useful for testing)
|
|
634
|
+
*/
|
|
635
|
+
resetQueryStats() {
|
|
636
|
+
this.queryHistograms.traces.reset();
|
|
637
|
+
this.queryHistograms.logs.reset();
|
|
638
|
+
this.queryHistograms.metrics.reset();
|
|
639
|
+
this.queryHistograms.llmEvents.reset();
|
|
640
|
+
this.queryHistograms.evaluations.reset();
|
|
641
|
+
}
|
|
509
642
|
/**
|
|
510
643
|
* Try to use an index for a file, returning matching line numbers or null if full scan needed
|
|
511
644
|
*/
|
|
@@ -521,365 +654,438 @@ export class LocalJsonlBackend {
|
|
|
521
654
|
return queryIndex(index, indexOptions);
|
|
522
655
|
}
|
|
523
656
|
async queryTraces(options) {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
logTiming()
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (cached) {
|
|
540
|
-
logTiming();
|
|
541
|
-
return cached;
|
|
542
|
-
}
|
|
543
|
-
const files = getFilesInRange(this.telemetryDir, /traces-\d{4}-\d{2}-\d{2}\.jsonl(\.gz)?$/, options.startDate, options.endDate);
|
|
544
|
-
const results = [];
|
|
545
|
-
const limit = options.limit || 100;
|
|
546
|
-
const offset = options.offset || 0;
|
|
547
|
-
// Use cached regex to avoid recompilation for frequently-used patterns
|
|
548
|
-
const spanNameRegex = options.spanNameRegex
|
|
549
|
-
? getCachedRegex(options.spanNameRegex)
|
|
550
|
-
: null;
|
|
551
|
-
// Build index query options for indexable filters
|
|
552
|
-
const indexOptions = {
|
|
553
|
-
traceId: options.traceId,
|
|
554
|
-
spanName: options.spanName,
|
|
555
|
-
serviceName: options.serviceName,
|
|
556
|
-
};
|
|
557
|
-
// Helper to apply non-indexable filters to a span
|
|
558
|
-
const applyFilters = (span) => {
|
|
559
|
-
// Regex filter (not indexable)
|
|
560
|
-
if (spanNameRegex && !spanNameRegex.test(span.name))
|
|
561
|
-
return false;
|
|
562
|
-
if (options.excludeSpanName && span.name.includes(options.excludeSpanName))
|
|
563
|
-
return false;
|
|
564
|
-
if (options.minDurationMs && (span.durationMs || 0) < options.minDurationMs)
|
|
565
|
-
return false;
|
|
566
|
-
if (options.maxDurationMs && (span.durationMs || Infinity) > options.maxDurationMs)
|
|
567
|
-
return false;
|
|
568
|
-
// Apply attribute filter
|
|
569
|
-
if (options.attributeFilter) {
|
|
570
|
-
for (const [key, value] of Object.entries(options.attributeFilter)) {
|
|
571
|
-
if (span.attributes?.[key] !== value)
|
|
572
|
-
return false;
|
|
657
|
+
// P2 fix: Defer attribute allocation until span is actually created
|
|
658
|
+
return withSpan('obs_toolkit.query.traces', () => ({
|
|
659
|
+
'obs_toolkit.query.type': 'traces',
|
|
660
|
+
'obs_toolkit.query.backend': 'local',
|
|
661
|
+
'obs_toolkit.query.limit': options.limit ?? 100,
|
|
662
|
+
'obs_toolkit.query.start_date': options.startDate,
|
|
663
|
+
'obs_toolkit.query.end_date': options.endDate,
|
|
664
|
+
}), async (span) => {
|
|
665
|
+
const timer = startTiming();
|
|
666
|
+
const logTiming = () => {
|
|
667
|
+
const durationMs = timer.end();
|
|
668
|
+
recordQueryDuration('traces', durationMs, 'local');
|
|
669
|
+
this.queryHistograms.traces.observe(durationMs);
|
|
670
|
+
if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
|
|
671
|
+
console.warn(`[obs-toolkit] Slow query: queryTraces took ${durationMs.toFixed(1)}ms`);
|
|
573
672
|
}
|
|
673
|
+
};
|
|
674
|
+
// Check circuit breaker - fail fast if open
|
|
675
|
+
if (!this.circuitBreaker.canRequest()) {
|
|
676
|
+
logTiming();
|
|
677
|
+
if (span)
|
|
678
|
+
span.setAttribute('obs_toolkit.query.result_count', 0);
|
|
679
|
+
return [];
|
|
574
680
|
}
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
681
|
+
// Check cache first
|
|
682
|
+
const cacheKey = makeCacheKey('traces', options);
|
|
683
|
+
const cached = this.traceCache.get(cacheKey);
|
|
684
|
+
if (cached) {
|
|
685
|
+
logTiming();
|
|
686
|
+
// M3 fix: Record cache hit for metrics and span event
|
|
687
|
+
recordCacheHit('traces');
|
|
688
|
+
if (span) {
|
|
689
|
+
span.addEvent('cache_hit');
|
|
690
|
+
span.setAttribute('obs_toolkit.query.result_count', cached.length);
|
|
580
691
|
}
|
|
692
|
+
return cached;
|
|
581
693
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
694
|
+
const files = getFilesInRange(this.telemetryDir, /traces-\d{4}-\d{2}-\d{2}\.jsonl(\.gz)?$/, options.startDate, options.endDate);
|
|
695
|
+
const results = [];
|
|
696
|
+
const limit = options.limit || 100;
|
|
697
|
+
const offset = options.offset || 0;
|
|
698
|
+
// Use cached regex to avoid recompilation for frequently-used patterns
|
|
699
|
+
const spanNameRegex = options.spanNameRegex
|
|
700
|
+
? getCachedRegex(options.spanNameRegex)
|
|
701
|
+
: null;
|
|
702
|
+
// Build index query options for indexable filters
|
|
703
|
+
const indexOptions = {
|
|
704
|
+
traceId: options.traceId,
|
|
705
|
+
spanName: options.spanName,
|
|
706
|
+
serviceName: options.serviceName,
|
|
707
|
+
};
|
|
708
|
+
// Helper to apply non-indexable filters to a span
|
|
709
|
+
const applyFilters = (s) => {
|
|
710
|
+
// Regex filter (not indexable)
|
|
711
|
+
if (spanNameRegex && !spanNameRegex.test(s.name))
|
|
712
|
+
return false;
|
|
713
|
+
if (options.excludeSpanName && s.name.includes(options.excludeSpanName))
|
|
714
|
+
return false;
|
|
715
|
+
if (options.minDurationMs && (s.durationMs || 0) < options.minDurationMs)
|
|
716
|
+
return false;
|
|
717
|
+
if (options.maxDurationMs && (s.durationMs || Infinity) > options.maxDurationMs)
|
|
718
|
+
return false;
|
|
719
|
+
// Apply attribute filter
|
|
720
|
+
if (options.attributeFilter) {
|
|
721
|
+
for (const [key, value] of Object.entries(options.attributeFilter)) {
|
|
722
|
+
if (s.attributes?.[key] !== value)
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Apply attributeExists filter
|
|
727
|
+
if (options.attributeExists) {
|
|
728
|
+
for (const key of options.attributeExists) {
|
|
729
|
+
if (s.attributes?.[key] === undefined)
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// Apply attributeNotExists filter
|
|
734
|
+
if (options.attributeNotExists) {
|
|
735
|
+
for (const key of options.attributeNotExists) {
|
|
736
|
+
if (s.attributes?.[key] !== undefined)
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
// Apply numeric filter conditions
|
|
741
|
+
if (options.numericFilter && options.numericFilter.length > 0) {
|
|
742
|
+
if (!applyNumericFilters(s.attributes, options.numericFilter))
|
|
586
743
|
return false;
|
|
587
744
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (options.numericFilter && options.numericFilter.length > 0) {
|
|
591
|
-
if (!applyNumericFilters(span.attributes, options.numericFilter))
|
|
745
|
+
// OTel GenAI agent/tool filters
|
|
746
|
+
if (options.agentId && s.attributes?.['gen_ai.agent.id'] !== options.agentId)
|
|
592
747
|
return false;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return paginated;
|
|
748
|
+
if (options.agentName && s.attributes?.['gen_ai.agent.name'] !== options.agentName)
|
|
749
|
+
return false;
|
|
750
|
+
if (options.toolName && s.attributes?.['gen_ai.tool.name'] !== options.toolName)
|
|
751
|
+
return false;
|
|
752
|
+
if (options.toolCallId && s.attributes?.['gen_ai.tool.call.id'] !== options.toolCallId)
|
|
753
|
+
return false;
|
|
754
|
+
if (options.toolType && s.attributes?.['gen_ai.tool.type'] !== options.toolType)
|
|
755
|
+
return false;
|
|
756
|
+
if (options.operationName && s.attributes?.['gen_ai.operation.name'] !== options.operationName)
|
|
757
|
+
return false;
|
|
758
|
+
return true;
|
|
759
|
+
};
|
|
760
|
+
try {
|
|
761
|
+
for (const file of files) {
|
|
762
|
+
// Try to use index for pre-filtering
|
|
763
|
+
const matchingLines = this.tryUseIndex(file, 'traces', indexOptions);
|
|
764
|
+
if (matchingLines !== null) {
|
|
765
|
+
// Use indexed query - read only matching lines
|
|
766
|
+
const rawRecords = await readLinesByNumber(file, matchingLines);
|
|
767
|
+
for (const raw of rawRecords) {
|
|
768
|
+
const traceSpan = normalizeSpan(raw);
|
|
769
|
+
if (!traceSpan)
|
|
770
|
+
continue;
|
|
771
|
+
if (!applyFilters(traceSpan))
|
|
772
|
+
continue;
|
|
773
|
+
results.push(traceSpan);
|
|
774
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
775
|
+
const paginated = paginateResults(results, offset, limit);
|
|
776
|
+
this.traceCache.set(cacheKey, paginated);
|
|
777
|
+
this.circuitBreaker.recordSuccess();
|
|
778
|
+
logTiming();
|
|
779
|
+
if (span)
|
|
780
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
781
|
+
return paginated;
|
|
782
|
+
}
|
|
629
783
|
}
|
|
630
784
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
if (!span)
|
|
637
|
-
continue;
|
|
638
|
-
// Apply indexable filters (since no index was used)
|
|
639
|
-
if (options.traceId && span.traceId !== options.traceId)
|
|
640
|
-
continue;
|
|
641
|
-
if (options.spanName && !span.name.includes(options.spanName))
|
|
642
|
-
continue;
|
|
643
|
-
if (options.serviceName) {
|
|
644
|
-
const svc = span.attributes?.['service.name'];
|
|
645
|
-
if (svc !== options.serviceName)
|
|
785
|
+
else {
|
|
786
|
+
// Fall back to full file scan
|
|
787
|
+
for await (const raw of streamJsonl(file, { statsTracker: this.parseStatsTracker })) {
|
|
788
|
+
const traceSpan = normalizeSpan(raw);
|
|
789
|
+
if (!traceSpan)
|
|
646
790
|
continue;
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
791
|
+
// Apply indexable filters (since no index was used)
|
|
792
|
+
if (options.traceId && traceSpan.traceId !== options.traceId)
|
|
793
|
+
continue;
|
|
794
|
+
if (options.spanName && !traceSpan.name.includes(options.spanName))
|
|
795
|
+
continue;
|
|
796
|
+
if (options.serviceName) {
|
|
797
|
+
const svc = traceSpan.attributes?.['service.name'];
|
|
798
|
+
if (svc !== options.serviceName)
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (!applyFilters(traceSpan))
|
|
802
|
+
continue;
|
|
803
|
+
results.push(traceSpan);
|
|
804
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
805
|
+
const paginated = paginateResults(results, offset, limit);
|
|
806
|
+
this.traceCache.set(cacheKey, paginated);
|
|
807
|
+
this.circuitBreaker.recordSuccess();
|
|
808
|
+
logTiming();
|
|
809
|
+
if (span)
|
|
810
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
811
|
+
return paginated;
|
|
812
|
+
}
|
|
657
813
|
}
|
|
658
814
|
}
|
|
659
815
|
}
|
|
816
|
+
const paginated = paginateResults(results, offset, limit);
|
|
817
|
+
this.traceCache.set(cacheKey, paginated);
|
|
818
|
+
this.circuitBreaker.recordSuccess();
|
|
819
|
+
logTiming();
|
|
820
|
+
if (span)
|
|
821
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
822
|
+
return paginated;
|
|
660
823
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
catch (error) {
|
|
668
|
-
this.circuitBreaker.recordFailure();
|
|
669
|
-
logTiming();
|
|
670
|
-
throw error;
|
|
671
|
-
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
this.circuitBreaker.recordFailure();
|
|
826
|
+
logTiming();
|
|
827
|
+
throw error;
|
|
828
|
+
}
|
|
829
|
+
});
|
|
672
830
|
}
|
|
673
831
|
async queryLogs(options) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
logTiming()
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (cached) {
|
|
690
|
-
logTiming();
|
|
691
|
-
return cached;
|
|
692
|
-
}
|
|
693
|
-
const files = getFilesInRange(this.telemetryDir, /logs-\d{4}-\d{2}-\d{2}\.jsonl(\.gz)?$/, options.startDate, options.endDate);
|
|
694
|
-
const results = [];
|
|
695
|
-
const limit = options.limit || 100;
|
|
696
|
-
const offset = options.offset || 0;
|
|
697
|
-
// Build index query options for indexable filters
|
|
698
|
-
const indexOptions = {
|
|
699
|
-
traceId: options.traceId,
|
|
700
|
-
severity: options.severity,
|
|
701
|
-
};
|
|
702
|
-
// Helper to apply non-indexable filters to a log
|
|
703
|
-
const applyFilters = (log) => {
|
|
704
|
-
if (options.search && !log.body.toLowerCase().includes(options.search.toLowerCase()))
|
|
705
|
-
return false;
|
|
706
|
-
// Apply boolean search with multiple terms
|
|
707
|
-
if (options.searchTerms && options.searchTerms.length > 0) {
|
|
708
|
-
const bodyLower = log.body.toLowerCase();
|
|
709
|
-
const operator = options.searchOperator || 'AND';
|
|
710
|
-
if (operator === 'AND') {
|
|
711
|
-
const allMatch = options.searchTerms.every(term => bodyLower.includes(term.toLowerCase()));
|
|
712
|
-
if (!allMatch)
|
|
713
|
-
return false;
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
const anyMatch = options.searchTerms.some(term => bodyLower.includes(term.toLowerCase()));
|
|
717
|
-
if (!anyMatch)
|
|
718
|
-
return false;
|
|
832
|
+
// P2 fix: Defer attribute allocation until span is actually created
|
|
833
|
+
return withSpan('obs_toolkit.query.logs', () => ({
|
|
834
|
+
'obs_toolkit.query.type': 'logs',
|
|
835
|
+
'obs_toolkit.query.backend': 'local',
|
|
836
|
+
'obs_toolkit.query.limit': options.limit ?? 100,
|
|
837
|
+
'obs_toolkit.query.start_date': options.startDate,
|
|
838
|
+
'obs_toolkit.query.end_date': options.endDate,
|
|
839
|
+
}), async (span) => {
|
|
840
|
+
const timer = startTiming();
|
|
841
|
+
const logTiming = () => {
|
|
842
|
+
const durationMs = timer.end();
|
|
843
|
+
recordQueryDuration('logs', durationMs, 'local');
|
|
844
|
+
this.queryHistograms.logs.observe(durationMs);
|
|
845
|
+
if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
|
|
846
|
+
console.warn(`[obs-toolkit] Slow query: queryLogs took ${durationMs.toFixed(1)}ms`);
|
|
719
847
|
}
|
|
848
|
+
};
|
|
849
|
+
// Check circuit breaker - fail fast if open
|
|
850
|
+
if (!this.circuitBreaker.canRequest()) {
|
|
851
|
+
logTiming();
|
|
852
|
+
if (span)
|
|
853
|
+
span.setAttribute('obs_toolkit.query.result_count', 0);
|
|
854
|
+
return [];
|
|
720
855
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
856
|
+
// Check cache first
|
|
857
|
+
const cacheKey = makeCacheKey('logs', options);
|
|
858
|
+
const cached = this.logCache.get(cacheKey);
|
|
859
|
+
if (cached) {
|
|
860
|
+
logTiming();
|
|
861
|
+
// M3 fix: Record cache hit for metrics and span event
|
|
862
|
+
recordCacheHit('logs');
|
|
863
|
+
if (span) {
|
|
864
|
+
span.addEvent('cache_hit');
|
|
865
|
+
span.setAttribute('obs_toolkit.query.result_count', cached.length);
|
|
728
866
|
}
|
|
867
|
+
return cached;
|
|
729
868
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
869
|
+
const files = getFilesInRange(this.telemetryDir, /logs-\d{4}-\d{2}-\d{2}\.jsonl(\.gz)?$/, options.startDate, options.endDate);
|
|
870
|
+
const results = [];
|
|
871
|
+
const limit = options.limit || 100;
|
|
872
|
+
const offset = options.offset || 0;
|
|
873
|
+
// Build index query options for indexable filters
|
|
874
|
+
const indexOptions = {
|
|
875
|
+
traceId: options.traceId,
|
|
876
|
+
severity: options.severity,
|
|
877
|
+
};
|
|
878
|
+
// Helper to apply non-indexable filters to a log
|
|
879
|
+
const applyFilters = (logRecord) => {
|
|
880
|
+
if (options.search && !logRecord.body.toLowerCase().includes(options.search.toLowerCase()))
|
|
881
|
+
return false;
|
|
882
|
+
// Apply boolean search with multiple terms
|
|
883
|
+
if (options.searchTerms && options.searchTerms.length > 0) {
|
|
884
|
+
const bodyLower = logRecord.body.toLowerCase();
|
|
885
|
+
const operator = options.searchOperator || 'AND';
|
|
886
|
+
if (operator === 'AND') {
|
|
887
|
+
const allMatch = options.searchTerms.every(term => bodyLower.includes(term.toLowerCase()));
|
|
888
|
+
if (!allMatch)
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
const anyMatch = options.searchTerms.some(term => bodyLower.includes(term.toLowerCase()));
|
|
893
|
+
if (!anyMatch)
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
735
896
|
}
|
|
736
|
-
|
|
737
|
-
// Apply numeric filter conditions
|
|
738
|
-
if (options.numericFilter && options.numericFilter.length > 0) {
|
|
739
|
-
if (!applyNumericFilters(log.attributes, options.numericFilter))
|
|
897
|
+
if (options.excludeSearch && logRecord.body.toLowerCase().includes(options.excludeSearch.toLowerCase()))
|
|
740
898
|
return false;
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
if (!log)
|
|
754
|
-
continue;
|
|
755
|
-
if (!applyFilters(log))
|
|
756
|
-
continue;
|
|
757
|
-
results.push(log);
|
|
758
|
-
if (hasReachedLimit(results.length, offset, limit)) {
|
|
759
|
-
const paginated = paginateResults(results, offset, limit);
|
|
760
|
-
this.logCache.set(cacheKey, paginated);
|
|
761
|
-
this.circuitBreaker.recordSuccess();
|
|
762
|
-
logTiming();
|
|
763
|
-
return paginated;
|
|
764
|
-
}
|
|
899
|
+
// Apply attributeExists filter
|
|
900
|
+
if (options.attributeExists) {
|
|
901
|
+
for (const key of options.attributeExists) {
|
|
902
|
+
if (logRecord.attributes?.[key] === undefined)
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
// Apply attributeNotExists filter
|
|
907
|
+
if (options.attributeNotExists) {
|
|
908
|
+
for (const key of options.attributeNotExists) {
|
|
909
|
+
if (logRecord.attributes?.[key] !== undefined)
|
|
910
|
+
return false;
|
|
765
911
|
}
|
|
766
912
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
913
|
+
// Apply numeric filter conditions
|
|
914
|
+
if (options.numericFilter && options.numericFilter.length > 0) {
|
|
915
|
+
if (!applyNumericFilters(logRecord.attributes, options.numericFilter))
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
return true;
|
|
919
|
+
};
|
|
920
|
+
try {
|
|
921
|
+
for (const file of files) {
|
|
922
|
+
// Try to use index for pre-filtering
|
|
923
|
+
const matchingLines = this.tryUseIndex(file, 'logs', indexOptions);
|
|
924
|
+
if (matchingLines !== null) {
|
|
925
|
+
// Use indexed query - read only matching lines
|
|
926
|
+
const rawRecords = await readLinesByNumber(file, matchingLines);
|
|
927
|
+
for (const raw of rawRecords) {
|
|
928
|
+
const logRecord = normalizeLog(raw, options.extractFields);
|
|
929
|
+
if (!logRecord)
|
|
930
|
+
continue;
|
|
931
|
+
if (!applyFilters(logRecord))
|
|
932
|
+
continue;
|
|
933
|
+
results.push(logRecord);
|
|
934
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
935
|
+
const paginated = paginateResults(results, offset, limit);
|
|
936
|
+
this.logCache.set(cacheKey, paginated);
|
|
937
|
+
this.circuitBreaker.recordSuccess();
|
|
938
|
+
logTiming();
|
|
939
|
+
if (span)
|
|
940
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
941
|
+
return paginated;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
// Fall back to full file scan
|
|
947
|
+
for await (const raw of streamJsonl(file, { statsTracker: this.parseStatsTracker })) {
|
|
948
|
+
const logRecord = normalizeLog(raw, options.extractFields);
|
|
949
|
+
if (!logRecord)
|
|
950
|
+
continue;
|
|
951
|
+
// Apply indexable filters (since no index was used)
|
|
952
|
+
if (options.severity && logRecord.severity.toUpperCase() !== options.severity.toUpperCase())
|
|
953
|
+
continue;
|
|
954
|
+
if (options.traceId && logRecord.traceId !== options.traceId)
|
|
955
|
+
continue;
|
|
956
|
+
if (!applyFilters(logRecord))
|
|
957
|
+
continue;
|
|
958
|
+
results.push(logRecord);
|
|
959
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
960
|
+
const paginated = paginateResults(results, offset, limit);
|
|
961
|
+
this.logCache.set(cacheKey, paginated);
|
|
962
|
+
this.circuitBreaker.recordSuccess();
|
|
963
|
+
logTiming();
|
|
964
|
+
if (span)
|
|
965
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
966
|
+
return paginated;
|
|
967
|
+
}
|
|
787
968
|
}
|
|
788
969
|
}
|
|
789
970
|
}
|
|
971
|
+
const paginated = paginateResults(results, offset, limit);
|
|
972
|
+
this.logCache.set(cacheKey, paginated);
|
|
973
|
+
this.circuitBreaker.recordSuccess();
|
|
974
|
+
logTiming();
|
|
975
|
+
if (span)
|
|
976
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
977
|
+
return paginated;
|
|
790
978
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
catch (error) {
|
|
798
|
-
this.circuitBreaker.recordFailure();
|
|
799
|
-
logTiming();
|
|
800
|
-
throw error;
|
|
801
|
-
}
|
|
979
|
+
catch (error) {
|
|
980
|
+
this.circuitBreaker.recordFailure();
|
|
981
|
+
logTiming();
|
|
982
|
+
throw error;
|
|
983
|
+
}
|
|
984
|
+
});
|
|
802
985
|
}
|
|
803
986
|
async queryMetrics(options) {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
987
|
+
// P2 fix: Defer attribute allocation until span is actually created
|
|
988
|
+
return withSpan('obs_toolkit.query.metrics', () => ({
|
|
989
|
+
'obs_toolkit.query.type': 'metrics',
|
|
990
|
+
'obs_toolkit.query.backend': 'local',
|
|
991
|
+
'obs_toolkit.query.limit': options.limit ?? 100,
|
|
992
|
+
'obs_toolkit.query.start_date': options.startDate,
|
|
993
|
+
'obs_toolkit.query.end_date': options.endDate,
|
|
994
|
+
}), async (span) => {
|
|
995
|
+
const timer = startTiming();
|
|
996
|
+
const logTiming = () => {
|
|
997
|
+
const durationMs = timer.end();
|
|
998
|
+
recordQueryDuration('metrics', durationMs, 'local');
|
|
999
|
+
this.queryHistograms.metrics.observe(durationMs);
|
|
1000
|
+
if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
|
|
1001
|
+
console.warn(`[obs-toolkit] Slow query: queryMetrics took ${durationMs.toFixed(1)}ms`);
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
// Check circuit breaker - fail fast if open
|
|
1005
|
+
if (!this.circuitBreaker.canRequest()) {
|
|
1006
|
+
logTiming();
|
|
1007
|
+
if (span)
|
|
1008
|
+
span.setAttribute('obs_toolkit.query.result_count', 0);
|
|
1009
|
+
return [];
|
|
809
1010
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1011
|
+
// Check cache first
|
|
1012
|
+
const cacheKey = makeCacheKey('metrics', options);
|
|
1013
|
+
const cached = this.metricCache.get(cacheKey);
|
|
1014
|
+
if (cached) {
|
|
1015
|
+
logTiming();
|
|
1016
|
+
// M3 fix: Record cache hit for metrics and span event
|
|
1017
|
+
recordCacheHit('metrics');
|
|
1018
|
+
if (span) {
|
|
1019
|
+
span.addEvent('cache_hit');
|
|
1020
|
+
span.setAttribute('obs_toolkit.query.result_count', cached.length);
|
|
1021
|
+
}
|
|
1022
|
+
return cached;
|
|
1023
|
+
}
|
|
1024
|
+
const files = getFilesInRange(this.telemetryDir, /metrics-\d{4}-\d{2}-\d{2}\.jsonl(\.gz)?$/, options.startDate, options.endDate);
|
|
1025
|
+
const results = [];
|
|
1026
|
+
const limit = options.limit || 100;
|
|
1027
|
+
const offset = options.offset || 0;
|
|
1028
|
+
// Build index query options for indexable filters
|
|
1029
|
+
const indexOptions = {
|
|
1030
|
+
metricName: options.metricName,
|
|
1031
|
+
};
|
|
1032
|
+
try {
|
|
1033
|
+
outer: for (const file of files) {
|
|
1034
|
+
// Try to use index for pre-filtering
|
|
1035
|
+
const matchingLines = this.tryUseIndex(file, 'metrics', indexOptions);
|
|
1036
|
+
if (matchingLines !== null) {
|
|
1037
|
+
// Use indexed query - read only matching lines
|
|
1038
|
+
const rawRecords = await readLinesByNumber(file, matchingLines);
|
|
1039
|
+
for (const raw of rawRecords) {
|
|
1040
|
+
const point = normalizeMetric(raw);
|
|
1041
|
+
if (!point)
|
|
1042
|
+
continue;
|
|
1043
|
+
results.push(point);
|
|
1044
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
1045
|
+
break outer;
|
|
1046
|
+
}
|
|
845
1047
|
}
|
|
846
1048
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1049
|
+
else {
|
|
1050
|
+
// Fall back to full file scan
|
|
1051
|
+
for await (const raw of streamJsonl(file, { statsTracker: this.parseStatsTracker })) {
|
|
1052
|
+
const point = normalizeMetric(raw);
|
|
1053
|
+
if (!point)
|
|
1054
|
+
continue;
|
|
1055
|
+
// Apply filters (since no index was used)
|
|
1056
|
+
if (options.metricName && !point.name.includes(options.metricName))
|
|
1057
|
+
continue;
|
|
1058
|
+
results.push(point);
|
|
1059
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
1060
|
+
break outer;
|
|
1061
|
+
}
|
|
860
1062
|
}
|
|
861
1063
|
}
|
|
862
1064
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1065
|
+
// Apply aggregation if requested
|
|
1066
|
+
if (options.aggregation && results.length > 0) {
|
|
1067
|
+
const aggregated = this.aggregate(results, options.aggregation, options.groupBy, options.timeBucket);
|
|
1068
|
+
this.metricCache.set(cacheKey, aggregated);
|
|
1069
|
+
this.circuitBreaker.recordSuccess();
|
|
1070
|
+
logTiming();
|
|
1071
|
+
if (span)
|
|
1072
|
+
span.setAttribute('obs_toolkit.query.result_count', aggregated.length);
|
|
1073
|
+
return aggregated;
|
|
1074
|
+
}
|
|
1075
|
+
const paginated = paginateResults(results, offset, limit);
|
|
1076
|
+
this.metricCache.set(cacheKey, paginated);
|
|
868
1077
|
this.circuitBreaker.recordSuccess();
|
|
869
1078
|
logTiming();
|
|
870
|
-
|
|
1079
|
+
if (span)
|
|
1080
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
1081
|
+
return paginated;
|
|
871
1082
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
catch (error) {
|
|
879
|
-
this.circuitBreaker.recordFailure();
|
|
880
|
-
logTiming();
|
|
881
|
-
throw error;
|
|
882
|
-
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
this.circuitBreaker.recordFailure();
|
|
1085
|
+
logTiming();
|
|
1086
|
+
throw error;
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
883
1089
|
}
|
|
884
1090
|
aggregate(points, aggregation, groupBy, timeBucket) {
|
|
885
1091
|
// Parse time bucket if provided
|
|
@@ -987,247 +1193,304 @@ export class LocalJsonlBackend {
|
|
|
987
1193
|
return (lastPoint.value - firstPoint.value) / durationSeconds;
|
|
988
1194
|
}
|
|
989
1195
|
async queryLLMEvents(options) {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
logTiming()
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
// Build index query options - eventName maps to spanName in index
|
|
1014
|
-
const indexOptions = {
|
|
1015
|
-
spanName: options.eventName,
|
|
1016
|
-
};
|
|
1017
|
-
// Helper to apply non-indexable filters to an event
|
|
1018
|
-
const applyFilters = (event) => {
|
|
1019
|
-
if (options.model) {
|
|
1020
|
-
const model = event.attributes?.['gen_ai.request.model'] || event.attributes?.['model'];
|
|
1021
|
-
if (model !== options.model)
|
|
1022
|
-
return false;
|
|
1023
|
-
}
|
|
1024
|
-
if (options.provider) {
|
|
1025
|
-
// OTel GenAI compliant: gen_ai.provider.name -> gen_ai.system -> provider
|
|
1026
|
-
const provider = event.attributes?.['gen_ai.provider.name'] ||
|
|
1027
|
-
event.attributes?.['gen_ai.system'] ||
|
|
1028
|
-
event.attributes?.['provider'];
|
|
1029
|
-
if (provider !== options.provider)
|
|
1030
|
-
return false;
|
|
1031
|
-
}
|
|
1032
|
-
if (options.operationName) {
|
|
1033
|
-
const opName = event.attributes?.['gen_ai.operation.name'];
|
|
1034
|
-
if (opName !== options.operationName)
|
|
1035
|
-
return false;
|
|
1036
|
-
}
|
|
1037
|
-
if (options.conversationId) {
|
|
1038
|
-
const convId = event.attributes?.['gen_ai.conversation.id'];
|
|
1039
|
-
if (convId !== options.conversationId)
|
|
1040
|
-
return false;
|
|
1196
|
+
// P2 fix: Defer attribute allocation until span is actually created
|
|
1197
|
+
return withSpan('obs_toolkit.query.llm_events', () => ({
|
|
1198
|
+
'obs_toolkit.query.type': 'llm_events',
|
|
1199
|
+
'obs_toolkit.query.backend': 'local',
|
|
1200
|
+
'obs_toolkit.query.limit': options.limit ?? 100,
|
|
1201
|
+
'obs_toolkit.query.start_date': options.startDate,
|
|
1202
|
+
'obs_toolkit.query.end_date': options.endDate,
|
|
1203
|
+
}), async (span) => {
|
|
1204
|
+
const timer = startTiming();
|
|
1205
|
+
const logTiming = () => {
|
|
1206
|
+
const durationMs = timer.end();
|
|
1207
|
+
recordQueryDuration('llm_events', durationMs, 'local');
|
|
1208
|
+
this.queryHistograms.llmEvents.observe(durationMs);
|
|
1209
|
+
if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
|
|
1210
|
+
console.warn(`[obs-toolkit] Slow query: queryLLMEvents took ${durationMs.toFixed(1)}ms`);
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
// Check circuit breaker - fail fast if open
|
|
1214
|
+
if (!this.circuitBreaker.canRequest()) {
|
|
1215
|
+
logTiming();
|
|
1216
|
+
if (span)
|
|
1217
|
+
span.setAttribute('obs_toolkit.query.result_count', 0);
|
|
1218
|
+
return [];
|
|
1041
1219
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1220
|
+
// Check cache first
|
|
1221
|
+
const cacheKey = makeCacheKey('llm-events', options);
|
|
1222
|
+
const cached = this.llmEventCache.get(cacheKey);
|
|
1223
|
+
if (cached) {
|
|
1224
|
+
logTiming();
|
|
1225
|
+
// M3 fix: Record cache hit for metrics and span event
|
|
1226
|
+
recordCacheHit('llmEvents');
|
|
1227
|
+
if (span) {
|
|
1228
|
+
span.addEvent('cache_hit');
|
|
1229
|
+
span.setAttribute('obs_toolkit.query.result_count', cached.length);
|
|
1230
|
+
}
|
|
1231
|
+
return cached;
|
|
1047
1232
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1233
|
+
const files = getFilesInRange(this.telemetryDir, /llm-events-\d{4}-\d{2}-\d{2}\.jsonl(\.gz)?$/, options.startDate, options.endDate);
|
|
1234
|
+
const results = [];
|
|
1235
|
+
const limit = options.limit || 100;
|
|
1236
|
+
const offset = options.offset || 0;
|
|
1237
|
+
// Build index query options - eventName maps to spanName in index
|
|
1238
|
+
const indexOptions = {
|
|
1239
|
+
spanName: options.eventName,
|
|
1240
|
+
};
|
|
1241
|
+
// Helper to apply non-indexable filters to an event
|
|
1242
|
+
const applyFilters = (event) => {
|
|
1243
|
+
if (options.model) {
|
|
1244
|
+
const model = event.attributes?.['gen_ai.request.model'] || event.attributes?.['model'];
|
|
1245
|
+
if (model !== options.model)
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
if (options.provider) {
|
|
1249
|
+
// OTel GenAI compliant: gen_ai.provider.name -> gen_ai.system -> provider
|
|
1250
|
+
const provider = event.attributes?.['gen_ai.provider.name'] ||
|
|
1251
|
+
event.attributes?.['gen_ai.system'] ||
|
|
1252
|
+
event.attributes?.['provider'];
|
|
1253
|
+
if (provider !== options.provider)
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
if (options.operationName) {
|
|
1257
|
+
const opName = event.attributes?.['gen_ai.operation.name'];
|
|
1258
|
+
if (opName !== options.operationName)
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
if (options.conversationId) {
|
|
1262
|
+
const convId = event.attributes?.['gen_ai.conversation.id'];
|
|
1263
|
+
if (convId !== options.conversationId)
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
if (options.search) {
|
|
1267
|
+
const searchLower = options.search.toLowerCase();
|
|
1268
|
+
const attrStr = JSON.stringify(event.attributes).toLowerCase();
|
|
1269
|
+
if (!attrStr.includes(searchLower) && !event.name.toLowerCase().includes(searchLower))
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
return true;
|
|
1273
|
+
};
|
|
1274
|
+
try {
|
|
1275
|
+
for (const file of files) {
|
|
1276
|
+
// Try to use index for pre-filtering
|
|
1277
|
+
const matchingLines = this.tryUseIndex(file, 'llm-events', indexOptions);
|
|
1278
|
+
if (matchingLines !== null) {
|
|
1279
|
+
// Use indexed query - read only matching lines
|
|
1280
|
+
const rawRecords = await readLinesByNumber(file, matchingLines);
|
|
1281
|
+
for (const event of rawRecords) {
|
|
1282
|
+
if (!event.timestamp || !event.name)
|
|
1283
|
+
continue;
|
|
1284
|
+
if (!applyFilters(event))
|
|
1285
|
+
continue;
|
|
1286
|
+
results.push({
|
|
1287
|
+
timestamp: event.timestamp,
|
|
1288
|
+
name: event.name,
|
|
1289
|
+
attributes: event.attributes,
|
|
1290
|
+
});
|
|
1291
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
1292
|
+
const paginated = paginateResults(results, offset, limit);
|
|
1293
|
+
this.llmEventCache.set(cacheKey, paginated);
|
|
1294
|
+
this.circuitBreaker.recordSuccess();
|
|
1295
|
+
logTiming();
|
|
1296
|
+
if (span)
|
|
1297
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
1298
|
+
return paginated;
|
|
1299
|
+
}
|
|
1073
1300
|
}
|
|
1074
1301
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1302
|
+
else {
|
|
1303
|
+
// Fall back to full file scan
|
|
1304
|
+
for await (const event of streamJsonl(file, { statsTracker: this.parseStatsTracker })) {
|
|
1305
|
+
if (!event.timestamp || !event.name)
|
|
1306
|
+
continue;
|
|
1307
|
+
// Apply indexable filters (since no index was used)
|
|
1308
|
+
if (options.eventName && !event.name.includes(options.eventName))
|
|
1309
|
+
continue;
|
|
1310
|
+
if (!applyFilters(event))
|
|
1311
|
+
continue;
|
|
1312
|
+
results.push({
|
|
1313
|
+
timestamp: event.timestamp,
|
|
1314
|
+
name: event.name,
|
|
1315
|
+
attributes: event.attributes,
|
|
1316
|
+
});
|
|
1317
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
1318
|
+
const paginated = paginateResults(results, offset, limit);
|
|
1319
|
+
this.llmEventCache.set(cacheKey, paginated);
|
|
1320
|
+
this.circuitBreaker.recordSuccess();
|
|
1321
|
+
logTiming();
|
|
1322
|
+
if (span)
|
|
1323
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
1324
|
+
return paginated;
|
|
1325
|
+
}
|
|
1097
1326
|
}
|
|
1098
1327
|
}
|
|
1099
1328
|
}
|
|
1329
|
+
const paginated = paginateResults(results, offset, limit);
|
|
1330
|
+
this.llmEventCache.set(cacheKey, paginated);
|
|
1331
|
+
this.circuitBreaker.recordSuccess();
|
|
1332
|
+
logTiming();
|
|
1333
|
+
if (span)
|
|
1334
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
1335
|
+
return paginated;
|
|
1100
1336
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
}
|
|
1107
|
-
catch (error) {
|
|
1108
|
-
this.circuitBreaker.recordFailure();
|
|
1109
|
-
logTiming();
|
|
1110
|
-
throw error;
|
|
1111
|
-
}
|
|
1337
|
+
catch (error) {
|
|
1338
|
+
this.circuitBreaker.recordFailure();
|
|
1339
|
+
logTiming();
|
|
1340
|
+
throw error;
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1112
1343
|
}
|
|
1113
1344
|
async queryEvaluations(options) {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1345
|
+
// P2 fix: Defer attribute allocation until span is actually created
|
|
1346
|
+
return withSpan('obs_toolkit.query.evaluations', () => ({
|
|
1347
|
+
'obs_toolkit.query.type': 'evaluations',
|
|
1348
|
+
'obs_toolkit.query.backend': 'local',
|
|
1349
|
+
'obs_toolkit.query.limit': options.limit ?? 100,
|
|
1350
|
+
'obs_toolkit.query.start_date': options.startDate,
|
|
1351
|
+
'obs_toolkit.query.end_date': options.endDate,
|
|
1352
|
+
}), async (span) => {
|
|
1353
|
+
const timer = startTiming();
|
|
1354
|
+
const logTiming = () => {
|
|
1355
|
+
const durationMs = timer.end();
|
|
1356
|
+
recordQueryDuration('evaluations', durationMs, 'local');
|
|
1357
|
+
this.queryHistograms.evaluations.observe(durationMs);
|
|
1358
|
+
if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
|
|
1359
|
+
console.warn(`[obs-toolkit] Slow query: queryEvaluations took ${durationMs.toFixed(1)}ms`);
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
// Check circuit breaker - fail fast if open
|
|
1363
|
+
if (!this.circuitBreaker.canRequest()) {
|
|
1364
|
+
logTiming();
|
|
1365
|
+
if (span)
|
|
1366
|
+
span.setAttribute('obs_toolkit.query.result_count', 0);
|
|
1367
|
+
return [];
|
|
1119
1368
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
}
|
|
1133
|
-
const files = getFilesInRange(this.telemetryDir, /evaluations-\d{4}-\d{2}-\d{2}\.jsonl(\.gz)?$/, options.startDate, options.endDate);
|
|
1134
|
-
const results = [];
|
|
1135
|
-
const limit = options.limit || 100;
|
|
1136
|
-
const offset = options.offset || 0;
|
|
1137
|
-
// Build index query options for indexable filters
|
|
1138
|
-
const indexOptions = {
|
|
1139
|
-
traceId: options.traceId,
|
|
1140
|
-
evaluationName: options.evaluationName,
|
|
1141
|
-
scoreLabel: options.scoreLabel,
|
|
1142
|
-
responseId: options.responseId,
|
|
1143
|
-
evaluator: options.evaluator,
|
|
1144
|
-
};
|
|
1145
|
-
// Helper to apply non-indexable filters to an evaluation
|
|
1146
|
-
const applyFilters = (evaluation) => {
|
|
1147
|
-
// Score range filters - only apply when evaluation HAS a scoreValue (P1-1 fix)
|
|
1148
|
-
// Evaluations with only scoreLabel (qualitative) should pass through
|
|
1149
|
-
if (options.scoreMin !== undefined && evaluation.scoreValue !== undefined) {
|
|
1150
|
-
if (evaluation.scoreValue < options.scoreMin)
|
|
1151
|
-
return false;
|
|
1369
|
+
// Check cache first
|
|
1370
|
+
const cacheKey = makeCacheKey('evaluations', options);
|
|
1371
|
+
const cached = this.evaluationCache.get(cacheKey);
|
|
1372
|
+
if (cached) {
|
|
1373
|
+
logTiming();
|
|
1374
|
+
// M3 fix: Record cache hit for metrics and span event
|
|
1375
|
+
recordCacheHit('evaluations');
|
|
1376
|
+
if (span) {
|
|
1377
|
+
span.addEvent('cache_hit');
|
|
1378
|
+
span.setAttribute('obs_toolkit.query.result_count', cached.length);
|
|
1379
|
+
}
|
|
1380
|
+
return cached;
|
|
1152
1381
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1382
|
+
const files = getFilesInRange(this.telemetryDir, /evaluations-\d{4}-\d{2}-\d{2}\.jsonl(\.gz)?$/, options.startDate, options.endDate);
|
|
1383
|
+
const results = [];
|
|
1384
|
+
const limit = options.limit || 100;
|
|
1385
|
+
const offset = options.offset || 0;
|
|
1386
|
+
// Build index query options for indexable filters
|
|
1387
|
+
const indexOptions = {
|
|
1388
|
+
traceId: options.traceId,
|
|
1389
|
+
evaluationName: options.evaluationName,
|
|
1390
|
+
scoreLabel: options.scoreLabel,
|
|
1391
|
+
responseId: options.responseId,
|
|
1392
|
+
evaluator: options.evaluator,
|
|
1393
|
+
};
|
|
1394
|
+
// Helper to apply non-indexable filters to an evaluation
|
|
1395
|
+
const applyFilters = (evaluation) => {
|
|
1396
|
+
// Score range filters - only apply when evaluation HAS a scoreValue (P1-1 fix)
|
|
1397
|
+
// Evaluations with only scoreLabel (qualitative) should pass through
|
|
1398
|
+
if (options.scoreMin !== undefined && evaluation.scoreValue !== undefined) {
|
|
1399
|
+
if (evaluation.scoreValue < options.scoreMin)
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
if (options.scoreMax !== undefined && evaluation.scoreValue !== undefined) {
|
|
1403
|
+
if (evaluation.scoreValue > options.scoreMax)
|
|
1404
|
+
return false;
|
|
1405
|
+
}
|
|
1406
|
+
// Session filter
|
|
1407
|
+
if (options.sessionId && evaluation.sessionId !== options.sessionId) {
|
|
1155
1408
|
return false;
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
//
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1409
|
+
}
|
|
1410
|
+
// Evaluator type filter (not indexed, so always applied here)
|
|
1411
|
+
if (options.evaluatorType && evaluation.evaluatorType !== options.evaluatorType) {
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
// Agent-as-Judge filters (Section 10.7)
|
|
1415
|
+
if (options.agentId && evaluation.agentId !== options.agentId) {
|
|
1416
|
+
return false;
|
|
1417
|
+
}
|
|
1418
|
+
if (options.agentName && evaluation.agentName !== options.agentName) {
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
return true;
|
|
1422
|
+
};
|
|
1423
|
+
try {
|
|
1424
|
+
for (const file of files) {
|
|
1425
|
+
// Try to use index for pre-filtering
|
|
1426
|
+
const matchingLines = this.tryUseIndex(file, 'evaluations', indexOptions);
|
|
1427
|
+
if (matchingLines !== null) {
|
|
1428
|
+
// Use indexed query - read only matching lines
|
|
1429
|
+
const rawRecords = await readLinesByNumber(file, matchingLines);
|
|
1430
|
+
for (const raw of rawRecords) {
|
|
1431
|
+
const evaluation = normalizeEvaluation(raw);
|
|
1432
|
+
if (!evaluation)
|
|
1433
|
+
continue;
|
|
1434
|
+
if (!applyFilters(evaluation))
|
|
1435
|
+
continue;
|
|
1436
|
+
results.push(evaluation);
|
|
1437
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
1438
|
+
const paginated = paginateResults(results, offset, limit);
|
|
1439
|
+
this.evaluationCache.set(cacheKey, paginated);
|
|
1440
|
+
this.circuitBreaker.recordSuccess();
|
|
1441
|
+
logTiming();
|
|
1442
|
+
if (span)
|
|
1443
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
1444
|
+
return paginated;
|
|
1445
|
+
}
|
|
1187
1446
|
}
|
|
1188
1447
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1448
|
+
else {
|
|
1449
|
+
// Fall back to full file scan
|
|
1450
|
+
for await (const raw of streamJsonl(file, { statsTracker: this.parseStatsTracker })) {
|
|
1451
|
+
const evaluation = normalizeEvaluation(raw);
|
|
1452
|
+
if (!evaluation)
|
|
1453
|
+
continue;
|
|
1454
|
+
// Apply indexable filters (since no index was used)
|
|
1455
|
+
if (options.traceId && evaluation.traceId !== options.traceId)
|
|
1456
|
+
continue;
|
|
1457
|
+
if (options.evaluationName && !evaluation.evaluationName.includes(options.evaluationName))
|
|
1458
|
+
continue;
|
|
1459
|
+
if (options.scoreLabel && evaluation.scoreLabel !== options.scoreLabel)
|
|
1460
|
+
continue;
|
|
1461
|
+
if (options.responseId && evaluation.responseId !== options.responseId)
|
|
1462
|
+
continue;
|
|
1463
|
+
if (options.evaluator && evaluation.evaluator !== options.evaluator)
|
|
1464
|
+
continue;
|
|
1465
|
+
if (!applyFilters(evaluation))
|
|
1466
|
+
continue;
|
|
1467
|
+
results.push(evaluation);
|
|
1468
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
1469
|
+
const paginated = paginateResults(results, offset, limit);
|
|
1470
|
+
this.evaluationCache.set(cacheKey, paginated);
|
|
1471
|
+
this.circuitBreaker.recordSuccess();
|
|
1472
|
+
logTiming();
|
|
1473
|
+
if (span)
|
|
1474
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
1475
|
+
return paginated;
|
|
1476
|
+
}
|
|
1216
1477
|
}
|
|
1217
1478
|
}
|
|
1218
1479
|
}
|
|
1480
|
+
const paginated = paginateResults(results, offset, limit);
|
|
1481
|
+
this.evaluationCache.set(cacheKey, paginated);
|
|
1482
|
+
this.circuitBreaker.recordSuccess();
|
|
1483
|
+
logTiming();
|
|
1484
|
+
if (span)
|
|
1485
|
+
span.setAttribute('obs_toolkit.query.result_count', paginated.length);
|
|
1486
|
+
return paginated;
|
|
1219
1487
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
}
|
|
1226
|
-
catch (error) {
|
|
1227
|
-
this.circuitBreaker.recordFailure();
|
|
1228
|
-
logTiming();
|
|
1229
|
-
throw error;
|
|
1230
|
-
}
|
|
1488
|
+
catch (error) {
|
|
1489
|
+
this.circuitBreaker.recordFailure();
|
|
1490
|
+
logTiming();
|
|
1491
|
+
throw error;
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1231
1494
|
}
|
|
1232
1495
|
async healthCheck() {
|
|
1233
1496
|
// Check circuit breaker state
|
|
@@ -1410,6 +1673,105 @@ export class MultiDirectoryBackend {
|
|
|
1410
1673
|
}
|
|
1411
1674
|
return result;
|
|
1412
1675
|
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Get aggregated parse statistics from all backends
|
|
1678
|
+
*/
|
|
1679
|
+
getParseStats() {
|
|
1680
|
+
// Aggregate stats from all backends
|
|
1681
|
+
let totalFiles = 0;
|
|
1682
|
+
let totalLines = 0;
|
|
1683
|
+
let totalParsed = 0;
|
|
1684
|
+
let totalSkipped = 0;
|
|
1685
|
+
const allWorstFiles = [];
|
|
1686
|
+
for (const backend of this.backends) {
|
|
1687
|
+
const stats = backend.getParseStats();
|
|
1688
|
+
totalFiles += stats.totalFiles;
|
|
1689
|
+
totalLines += stats.totalLines;
|
|
1690
|
+
totalParsed += stats.totalParsed;
|
|
1691
|
+
totalSkipped += stats.totalSkipped;
|
|
1692
|
+
// Collect worst files with skip rates
|
|
1693
|
+
for (const file of stats.worstFiles) {
|
|
1694
|
+
const totalProcessed = file.parsedLines + file.skippedLines;
|
|
1695
|
+
if (totalProcessed > 0) {
|
|
1696
|
+
allWorstFiles.push({
|
|
1697
|
+
...file,
|
|
1698
|
+
skipRate: file.skippedLines / totalProcessed,
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
// Calculate overall success rate
|
|
1704
|
+
const totalProcessed = totalParsed + totalSkipped;
|
|
1705
|
+
const parseSuccessRate = totalProcessed > 0 ? totalParsed / totalProcessed : 1;
|
|
1706
|
+
// Get top worst files across all backends
|
|
1707
|
+
const worstFiles = allWorstFiles
|
|
1708
|
+
.sort((a, b) => b.skipRate - a.skipRate)
|
|
1709
|
+
.slice(0, WORST_FILES_LIMIT)
|
|
1710
|
+
.map(({ skipRate: _, ...f }) => f);
|
|
1711
|
+
return {
|
|
1712
|
+
totalFiles,
|
|
1713
|
+
totalLines,
|
|
1714
|
+
totalParsed,
|
|
1715
|
+
totalSkipped,
|
|
1716
|
+
parseSuccessRate,
|
|
1717
|
+
worstFiles,
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Get aggregated query latency statistics from all backends
|
|
1722
|
+
*/
|
|
1723
|
+
getQueryStats() {
|
|
1724
|
+
// Aggregate histogram data from all backends
|
|
1725
|
+
// For percentiles, we use the first backend's data since aggregating
|
|
1726
|
+
// percentiles across backends requires full data merge
|
|
1727
|
+
// Count and sum can be aggregated directly
|
|
1728
|
+
const types = ['traces', 'logs', 'metrics', 'llmEvents', 'evaluations'];
|
|
1729
|
+
const result = {};
|
|
1730
|
+
for (const type of types) {
|
|
1731
|
+
let totalCount = 0;
|
|
1732
|
+
let weightedP50 = 0;
|
|
1733
|
+
let weightedP95 = 0;
|
|
1734
|
+
let weightedP99 = 0;
|
|
1735
|
+
let minValue = Infinity;
|
|
1736
|
+
let maxValue = -Infinity;
|
|
1737
|
+
let totalSum = 0;
|
|
1738
|
+
for (const backend of this.backends) {
|
|
1739
|
+
const stats = backend.getQueryStats()[type];
|
|
1740
|
+
if (stats.count > 0) {
|
|
1741
|
+
totalCount += stats.count;
|
|
1742
|
+
totalSum += stats.mean * stats.count;
|
|
1743
|
+
weightedP50 += stats.p50 * stats.count;
|
|
1744
|
+
weightedP95 += stats.p95 * stats.count;
|
|
1745
|
+
weightedP99 += stats.p99 * stats.count;
|
|
1746
|
+
minValue = Math.min(minValue, stats.min);
|
|
1747
|
+
maxValue = Math.max(maxValue, stats.max);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (totalCount > 0) {
|
|
1751
|
+
result[type] = {
|
|
1752
|
+
min: minValue,
|
|
1753
|
+
max: maxValue,
|
|
1754
|
+
mean: totalSum / totalCount,
|
|
1755
|
+
p50: weightedP50 / totalCount,
|
|
1756
|
+
p95: weightedP95 / totalCount,
|
|
1757
|
+
p99: weightedP99 / totalCount,
|
|
1758
|
+
count: totalCount,
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
else {
|
|
1762
|
+
result[type] = {
|
|
1763
|
+
min: 0,
|
|
1764
|
+
max: 0,
|
|
1765
|
+
mean: 0,
|
|
1766
|
+
p50: 0,
|
|
1767
|
+
p95: 0,
|
|
1768
|
+
p99: 0,
|
|
1769
|
+
count: 0,
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return result;
|
|
1774
|
+
}
|
|
1413
1775
|
/**
|
|
1414
1776
|
* Export traces in OTLP JSON format
|
|
1415
1777
|
*/
|