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.
Files changed (237) hide show
  1. package/README.md +126 -5
  2. package/dist/backends/index.d.ts +163 -0
  3. package/dist/backends/index.d.ts.map +1 -1
  4. package/dist/backends/index.js +57 -0
  5. package/dist/backends/index.js.map +1 -1
  6. package/dist/backends/index.test.js +55 -1
  7. package/dist/backends/index.test.js.map +1 -1
  8. package/dist/backends/local-jsonl.d.ts +30 -0
  9. package/dist/backends/local-jsonl.d.ts.map +1 -1
  10. package/dist/backends/local-jsonl.js +912 -550
  11. package/dist/backends/local-jsonl.js.map +1 -1
  12. package/dist/backends/signoz-api-rate-limiter.test.js +2 -1
  13. package/dist/backends/signoz-api-rate-limiter.test.js.map +1 -1
  14. package/dist/backends/signoz-api.d.ts +16 -2
  15. package/dist/backends/signoz-api.d.ts.map +1 -1
  16. package/dist/backends/signoz-api.js +650 -534
  17. package/dist/backends/signoz-api.js.map +1 -1
  18. package/dist/backends/signoz-api.test.js +6 -5
  19. package/dist/backends/signoz-api.test.js.map +1 -1
  20. package/dist/lib/agent-as-judge.d.ts +388 -0
  21. package/dist/lib/agent-as-judge.d.ts.map +1 -0
  22. package/dist/lib/agent-as-judge.js +740 -0
  23. package/dist/lib/agent-as-judge.js.map +1 -0
  24. package/dist/lib/agent-as-judge.test.d.ts +5 -0
  25. package/dist/lib/agent-as-judge.test.d.ts.map +1 -0
  26. package/dist/lib/agent-as-judge.test.js +816 -0
  27. package/dist/lib/agent-as-judge.test.js.map +1 -0
  28. package/dist/lib/cache.d.ts +15 -2
  29. package/dist/lib/cache.d.ts.map +1 -1
  30. package/dist/lib/cache.js +16 -2
  31. package/dist/lib/cache.js.map +1 -1
  32. package/dist/lib/circuit-breaker.d.ts +18 -0
  33. package/dist/lib/circuit-breaker.d.ts.map +1 -1
  34. package/dist/lib/circuit-breaker.js +41 -8
  35. package/dist/lib/circuit-breaker.js.map +1 -1
  36. package/dist/lib/confident-export.d.ts +101 -0
  37. package/dist/lib/confident-export.d.ts.map +1 -0
  38. package/dist/lib/confident-export.js +393 -0
  39. package/dist/lib/confident-export.js.map +1 -0
  40. package/dist/lib/confident-export.test.d.ts +7 -0
  41. package/dist/lib/confident-export.test.d.ts.map +1 -0
  42. package/dist/lib/confident-export.test.js +835 -0
  43. package/dist/lib/confident-export.test.js.map +1 -0
  44. package/dist/lib/constants.d.ts +75 -0
  45. package/dist/lib/constants.d.ts.map +1 -1
  46. package/dist/lib/constants.js +104 -1
  47. package/dist/lib/constants.js.map +1 -1
  48. package/dist/lib/datadog-export.d.ts +156 -0
  49. package/dist/lib/datadog-export.d.ts.map +1 -0
  50. package/dist/lib/datadog-export.js +464 -0
  51. package/dist/lib/datadog-export.js.map +1 -0
  52. package/dist/lib/datadog-export.test.d.ts +14 -0
  53. package/dist/lib/datadog-export.test.d.ts.map +1 -0
  54. package/dist/lib/datadog-export.test.js +890 -0
  55. package/dist/lib/datadog-export.test.js.map +1 -0
  56. package/dist/lib/evaluation-hooks.d.ts +49 -0
  57. package/dist/lib/evaluation-hooks.d.ts.map +1 -0
  58. package/dist/lib/evaluation-hooks.js +488 -0
  59. package/dist/lib/evaluation-hooks.js.map +1 -0
  60. package/dist/lib/evaluation-hooks.test.d.ts +8 -0
  61. package/dist/lib/evaluation-hooks.test.d.ts.map +1 -0
  62. package/dist/lib/evaluation-hooks.test.js +624 -0
  63. package/dist/lib/evaluation-hooks.test.js.map +1 -0
  64. package/dist/lib/export-utils.d.ts +99 -0
  65. package/dist/lib/export-utils.d.ts.map +1 -0
  66. package/dist/lib/export-utils.js +238 -0
  67. package/dist/lib/export-utils.js.map +1 -0
  68. package/dist/lib/export-utils.test.d.ts +5 -0
  69. package/dist/lib/export-utils.test.d.ts.map +1 -0
  70. package/dist/lib/export-utils.test.js +193 -0
  71. package/dist/lib/export-utils.test.js.map +1 -0
  72. package/dist/lib/file-utils.d.ts +17 -2
  73. package/dist/lib/file-utils.d.ts.map +1 -1
  74. package/dist/lib/file-utils.js +24 -5
  75. package/dist/lib/file-utils.js.map +1 -1
  76. package/dist/lib/file-utils.test.js +30 -0
  77. package/dist/lib/file-utils.test.js.map +1 -1
  78. package/dist/lib/histogram.d.ts +119 -0
  79. package/dist/lib/histogram.d.ts.map +1 -0
  80. package/dist/lib/histogram.js +202 -0
  81. package/dist/lib/histogram.js.map +1 -0
  82. package/dist/lib/histogram.test.d.ts +5 -0
  83. package/dist/lib/histogram.test.d.ts.map +1 -0
  84. package/dist/lib/histogram.test.js +381 -0
  85. package/dist/lib/histogram.test.js.map +1 -0
  86. package/dist/lib/instrumentation.d.ts +153 -0
  87. package/dist/lib/instrumentation.d.ts.map +1 -0
  88. package/dist/lib/instrumentation.integration.test.d.ts +2 -0
  89. package/dist/lib/instrumentation.integration.test.d.ts.map +1 -0
  90. package/dist/lib/instrumentation.integration.test.js +589 -0
  91. package/dist/lib/instrumentation.integration.test.js.map +1 -0
  92. package/dist/lib/instrumentation.js +520 -0
  93. package/dist/lib/instrumentation.js.map +1 -0
  94. package/dist/lib/instrumentation.test.d.ts +2 -0
  95. package/dist/lib/instrumentation.test.d.ts.map +1 -0
  96. package/dist/lib/instrumentation.test.js +821 -0
  97. package/dist/lib/instrumentation.test.js.map +1 -0
  98. package/dist/lib/langfuse-export.d.ts +125 -0
  99. package/dist/lib/langfuse-export.d.ts.map +1 -0
  100. package/dist/lib/langfuse-export.js +367 -0
  101. package/dist/lib/langfuse-export.js.map +1 -0
  102. package/dist/lib/langfuse-export.test.d.ts +7 -0
  103. package/dist/lib/langfuse-export.test.d.ts.map +1 -0
  104. package/dist/lib/langfuse-export.test.js +1007 -0
  105. package/dist/lib/langfuse-export.test.js.map +1 -0
  106. package/dist/lib/llm-as-judge.d.ts +657 -0
  107. package/dist/lib/llm-as-judge.d.ts.map +1 -0
  108. package/dist/lib/llm-as-judge.js +1397 -0
  109. package/dist/lib/llm-as-judge.js.map +1 -0
  110. package/dist/lib/llm-as-judge.test.d.ts +2 -0
  111. package/dist/lib/llm-as-judge.test.d.ts.map +1 -0
  112. package/dist/lib/llm-as-judge.test.js +2409 -0
  113. package/dist/lib/llm-as-judge.test.js.map +1 -0
  114. package/dist/lib/logger.d.ts +1 -1
  115. package/dist/lib/logger.d.ts.map +1 -1
  116. package/dist/lib/logger.js.map +1 -1
  117. package/dist/lib/metrics.d.ts +62 -0
  118. package/dist/lib/metrics.d.ts.map +1 -0
  119. package/dist/lib/metrics.js +166 -0
  120. package/dist/lib/metrics.js.map +1 -0
  121. package/dist/lib/metrics.test.d.ts +5 -0
  122. package/dist/lib/metrics.test.d.ts.map +1 -0
  123. package/dist/lib/metrics.test.js +189 -0
  124. package/dist/lib/metrics.test.js.map +1 -0
  125. package/dist/lib/parse-stats.d.ts +119 -0
  126. package/dist/lib/parse-stats.d.ts.map +1 -0
  127. package/dist/lib/parse-stats.js +206 -0
  128. package/dist/lib/parse-stats.js.map +1 -0
  129. package/dist/lib/parse-stats.test.d.ts +5 -0
  130. package/dist/lib/parse-stats.test.d.ts.map +1 -0
  131. package/dist/lib/parse-stats.test.js +283 -0
  132. package/dist/lib/parse-stats.test.js.map +1 -0
  133. package/dist/lib/phoenix-export.d.ts +109 -0
  134. package/dist/lib/phoenix-export.d.ts.map +1 -0
  135. package/dist/lib/phoenix-export.js +429 -0
  136. package/dist/lib/phoenix-export.js.map +1 -0
  137. package/dist/lib/phoenix-export.test.d.ts +11 -0
  138. package/dist/lib/phoenix-export.test.d.ts.map +1 -0
  139. package/dist/lib/phoenix-export.test.js +725 -0
  140. package/dist/lib/phoenix-export.test.js.map +1 -0
  141. package/dist/lib/server-utils.d.ts +6 -1
  142. package/dist/lib/server-utils.d.ts.map +1 -1
  143. package/dist/lib/server-utils.js +9 -1
  144. package/dist/lib/server-utils.js.map +1 -1
  145. package/dist/lib/shared-schemas.d.ts +6 -0
  146. package/dist/lib/shared-schemas.d.ts.map +1 -1
  147. package/dist/lib/shared-schemas.js +11 -4
  148. package/dist/lib/shared-schemas.js.map +1 -1
  149. package/dist/lib/verification-events.d.ts +100 -0
  150. package/dist/lib/verification-events.d.ts.map +1 -0
  151. package/dist/lib/verification-events.js +162 -0
  152. package/dist/lib/verification-events.js.map +1 -0
  153. package/dist/lib/verification-events.test.d.ts +5 -0
  154. package/dist/lib/verification-events.test.d.ts.map +1 -0
  155. package/dist/lib/verification-events.test.js +193 -0
  156. package/dist/lib/verification-events.test.js.map +1 -0
  157. package/dist/server.d.ts +5 -0
  158. package/dist/server.d.ts.map +1 -1
  159. package/dist/server.js +77 -21
  160. package/dist/server.js.map +1 -1
  161. package/dist/tools/context-stats.d.ts.map +1 -1
  162. package/dist/tools/context-stats.js +6 -8
  163. package/dist/tools/context-stats.js.map +1 -1
  164. package/dist/tools/export-confident.d.ts +145 -0
  165. package/dist/tools/export-confident.d.ts.map +1 -0
  166. package/dist/tools/export-confident.js +134 -0
  167. package/dist/tools/export-confident.js.map +1 -0
  168. package/dist/tools/export-confident.test.d.ts +7 -0
  169. package/dist/tools/export-confident.test.d.ts.map +1 -0
  170. package/dist/tools/export-confident.test.js +332 -0
  171. package/dist/tools/export-confident.test.js.map +1 -0
  172. package/dist/tools/export-datadog.d.ts +160 -0
  173. package/dist/tools/export-datadog.d.ts.map +1 -0
  174. package/dist/tools/export-datadog.js +160 -0
  175. package/dist/tools/export-datadog.js.map +1 -0
  176. package/dist/tools/export-datadog.test.d.ts +8 -0
  177. package/dist/tools/export-datadog.test.d.ts.map +1 -0
  178. package/dist/tools/export-datadog.test.js +419 -0
  179. package/dist/tools/export-datadog.test.js.map +1 -0
  180. package/dist/tools/export-langfuse.d.ts +137 -0
  181. package/dist/tools/export-langfuse.d.ts.map +1 -0
  182. package/dist/tools/export-langfuse.js +131 -0
  183. package/dist/tools/export-langfuse.js.map +1 -0
  184. package/dist/tools/export-langfuse.test.d.ts +7 -0
  185. package/dist/tools/export-langfuse.test.d.ts.map +1 -0
  186. package/dist/tools/export-langfuse.test.js +303 -0
  187. package/dist/tools/export-langfuse.test.js.map +1 -0
  188. package/dist/tools/export-phoenix.d.ts +145 -0
  189. package/dist/tools/export-phoenix.d.ts.map +1 -0
  190. package/dist/tools/export-phoenix.js +135 -0
  191. package/dist/tools/export-phoenix.js.map +1 -0
  192. package/dist/tools/export-phoenix.test.d.ts +7 -0
  193. package/dist/tools/export-phoenix.test.d.ts.map +1 -0
  194. package/dist/tools/export-phoenix.test.js +316 -0
  195. package/dist/tools/export-phoenix.test.js.map +1 -0
  196. package/dist/tools/health-check.d.ts +26 -0
  197. package/dist/tools/health-check.d.ts.map +1 -1
  198. package/dist/tools/health-check.js +36 -7
  199. package/dist/tools/health-check.js.map +1 -1
  200. package/dist/tools/index.d.ts +6 -0
  201. package/dist/tools/index.d.ts.map +1 -1
  202. package/dist/tools/index.js +6 -0
  203. package/dist/tools/index.js.map +1 -1
  204. package/dist/tools/inject-evaluations.d.ts +1315 -0
  205. package/dist/tools/inject-evaluations.d.ts.map +1 -0
  206. package/dist/tools/inject-evaluations.js +121 -0
  207. package/dist/tools/inject-evaluations.js.map +1 -0
  208. package/dist/tools/inject-evaluations.test.d.ts +5 -0
  209. package/dist/tools/inject-evaluations.test.d.ts.map +1 -0
  210. package/dist/tools/inject-evaluations.test.js +359 -0
  211. package/dist/tools/inject-evaluations.test.js.map +1 -0
  212. package/dist/tools/query-evaluations.d.ts +25 -4
  213. package/dist/tools/query-evaluations.d.ts.map +1 -1
  214. package/dist/tools/query-evaluations.js +10 -0
  215. package/dist/tools/query-evaluations.js.map +1 -1
  216. package/dist/tools/query-llm-events.js +2 -2
  217. package/dist/tools/query-llm-events.js.map +1 -1
  218. package/dist/tools/query-logs.d.ts +8 -8
  219. package/dist/tools/query-logs.js +3 -3
  220. package/dist/tools/query-logs.js.map +1 -1
  221. package/dist/tools/query-metrics.d.ts +4 -4
  222. package/dist/tools/query-metrics.js +2 -2
  223. package/dist/tools/query-metrics.js.map +1 -1
  224. package/dist/tools/query-traces.d.ts +8 -8
  225. package/dist/tools/query-verifications.d.ts +111 -0
  226. package/dist/tools/query-verifications.d.ts.map +1 -0
  227. package/dist/tools/query-verifications.js +101 -0
  228. package/dist/tools/query-verifications.js.map +1 -0
  229. package/dist/tools/query-verifications.test.d.ts +5 -0
  230. package/dist/tools/query-verifications.test.d.ts.map +1 -0
  231. package/dist/tools/query-verifications.test.js +156 -0
  232. package/dist/tools/query-verifications.test.js.map +1 -0
  233. package/dist/types/evaluation-hooks.d.ts +176 -0
  234. package/dist/types/evaluation-hooks.d.ts.map +1 -0
  235. package/dist/types/evaluation-hooks.js +49 -0
  236. package/dist/types/evaluation-hooks.js.map +1 -0
  237. 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
- console.warn(`Invalid regex pattern: ${pattern}`);
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
- const timer = startTiming();
525
- const logTiming = () => {
526
- const durationMs = timer.end();
527
- if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
528
- console.warn(`[obs-toolkit] Slow query: queryTraces took ${durationMs.toFixed(1)}ms`);
529
- }
530
- };
531
- // Check circuit breaker - fail fast if open
532
- if (!this.circuitBreaker.canRequest()) {
533
- logTiming();
534
- return [];
535
- }
536
- // Check cache first
537
- const cacheKey = makeCacheKey('traces', options);
538
- const cached = this.traceCache.get(cacheKey);
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
- // Apply attributeExists filter
576
- if (options.attributeExists) {
577
- for (const key of options.attributeExists) {
578
- if (span.attributes?.[key] === undefined)
579
- return false;
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
- // Apply attributeNotExists filter
583
- if (options.attributeNotExists) {
584
- for (const key of options.attributeNotExists) {
585
- if (span.attributes?.[key] !== undefined)
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
- // Apply numeric filter conditions
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
- // OTel GenAI agent/tool filters
595
- if (options.agentId && span.attributes?.['gen_ai.agent.id'] !== options.agentId)
596
- return false;
597
- if (options.agentName && span.attributes?.['gen_ai.agent.name'] !== options.agentName)
598
- return false;
599
- if (options.toolName && span.attributes?.['gen_ai.tool.name'] !== options.toolName)
600
- return false;
601
- if (options.toolCallId && span.attributes?.['gen_ai.tool.call.id'] !== options.toolCallId)
602
- return false;
603
- if (options.toolType && span.attributes?.['gen_ai.tool.type'] !== options.toolType)
604
- return false;
605
- if (options.operationName && span.attributes?.['gen_ai.operation.name'] !== options.operationName)
606
- return false;
607
- return true;
608
- };
609
- try {
610
- for (const file of files) {
611
- // Try to use index for pre-filtering
612
- const matchingLines = this.tryUseIndex(file, 'traces', indexOptions);
613
- if (matchingLines !== null) {
614
- // Use indexed query - read only matching lines
615
- const rawRecords = await readLinesByNumber(file, matchingLines);
616
- for (const raw of rawRecords) {
617
- const span = normalizeSpan(raw);
618
- if (!span)
619
- continue;
620
- if (!applyFilters(span))
621
- continue;
622
- results.push(span);
623
- if (hasReachedLimit(results.length, offset, limit)) {
624
- const paginated = paginateResults(results, offset, limit);
625
- this.traceCache.set(cacheKey, paginated);
626
- this.circuitBreaker.recordSuccess();
627
- logTiming();
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
- else {
633
- // Fall back to full file scan
634
- for await (const raw of streamJsonl(file)) {
635
- const span = normalizeSpan(raw);
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
- if (!applyFilters(span))
649
- continue;
650
- results.push(span);
651
- if (hasReachedLimit(results.length, offset, limit)) {
652
- const paginated = paginateResults(results, offset, limit);
653
- this.traceCache.set(cacheKey, paginated);
654
- this.circuitBreaker.recordSuccess();
655
- logTiming();
656
- return paginated;
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
- const paginated = paginateResults(results, offset, limit);
662
- this.traceCache.set(cacheKey, paginated);
663
- this.circuitBreaker.recordSuccess();
664
- logTiming();
665
- return paginated;
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
- const timer = startTiming();
675
- const logTiming = () => {
676
- const durationMs = timer.end();
677
- if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
678
- console.warn(`[obs-toolkit] Slow query: queryLogs took ${durationMs.toFixed(1)}ms`);
679
- }
680
- };
681
- // Check circuit breaker - fail fast if open
682
- if (!this.circuitBreaker.canRequest()) {
683
- logTiming();
684
- return [];
685
- }
686
- // Check cache first
687
- const cacheKey = makeCacheKey('logs', options);
688
- const cached = this.logCache.get(cacheKey);
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
- if (options.excludeSearch && log.body.toLowerCase().includes(options.excludeSearch.toLowerCase()))
722
- return false;
723
- // Apply attributeExists filter
724
- if (options.attributeExists) {
725
- for (const key of options.attributeExists) {
726
- if (log.attributes?.[key] === undefined)
727
- return false;
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
- // Apply attributeNotExists filter
731
- if (options.attributeNotExists) {
732
- for (const key of options.attributeNotExists) {
733
- if (log.attributes?.[key] !== undefined)
734
- return false;
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
- return true;
743
- };
744
- try {
745
- for (const file of files) {
746
- // Try to use index for pre-filtering
747
- const matchingLines = this.tryUseIndex(file, 'logs', indexOptions);
748
- if (matchingLines !== null) {
749
- // Use indexed query - read only matching lines
750
- const rawRecords = await readLinesByNumber(file, matchingLines);
751
- for (const raw of rawRecords) {
752
- const log = normalizeLog(raw, options.extractFields);
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
- else {
768
- // Fall back to full file scan
769
- for await (const raw of streamJsonl(file)) {
770
- const log = normalizeLog(raw, options.extractFields);
771
- if (!log)
772
- continue;
773
- // Apply indexable filters (since no index was used)
774
- if (options.severity && log.severity.toUpperCase() !== options.severity.toUpperCase())
775
- continue;
776
- if (options.traceId && log.traceId !== options.traceId)
777
- continue;
778
- if (!applyFilters(log))
779
- continue;
780
- results.push(log);
781
- if (hasReachedLimit(results.length, offset, limit)) {
782
- const paginated = paginateResults(results, offset, limit);
783
- this.logCache.set(cacheKey, paginated);
784
- this.circuitBreaker.recordSuccess();
785
- logTiming();
786
- return paginated;
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
- const paginated = paginateResults(results, offset, limit);
792
- this.logCache.set(cacheKey, paginated);
793
- this.circuitBreaker.recordSuccess();
794
- logTiming();
795
- return paginated;
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
- const timer = startTiming();
805
- const logTiming = () => {
806
- const durationMs = timer.end();
807
- if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
808
- console.warn(`[obs-toolkit] Slow query: queryMetrics took ${durationMs.toFixed(1)}ms`);
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
- // Check circuit breaker - fail fast if open
812
- if (!this.circuitBreaker.canRequest()) {
813
- logTiming();
814
- return [];
815
- }
816
- // Check cache first
817
- const cacheKey = makeCacheKey('metrics', options);
818
- const cached = this.metricCache.get(cacheKey);
819
- if (cached) {
820
- logTiming();
821
- return cached;
822
- }
823
- const files = getFilesInRange(this.telemetryDir, /metrics-\d{4}-\d{2}-\d{2}\.jsonl(\.gz)?$/, options.startDate, options.endDate);
824
- const results = [];
825
- const limit = options.limit || 100;
826
- const offset = options.offset || 0;
827
- // Build index query options for indexable filters
828
- const indexOptions = {
829
- metricName: options.metricName,
830
- };
831
- try {
832
- outer: for (const file of files) {
833
- // Try to use index for pre-filtering
834
- const matchingLines = this.tryUseIndex(file, 'metrics', indexOptions);
835
- if (matchingLines !== null) {
836
- // Use indexed query - read only matching lines
837
- const rawRecords = await readLinesByNumber(file, matchingLines);
838
- for (const raw of rawRecords) {
839
- const point = normalizeMetric(raw);
840
- if (!point)
841
- continue;
842
- results.push(point);
843
- if (hasReachedLimit(results.length, offset, limit)) {
844
- break outer;
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
- else {
849
- // Fall back to full file scan
850
- for await (const raw of streamJsonl(file)) {
851
- const point = normalizeMetric(raw);
852
- if (!point)
853
- continue;
854
- // Apply filters (since no index was used)
855
- if (options.metricName && !point.name.includes(options.metricName))
856
- continue;
857
- results.push(point);
858
- if (hasReachedLimit(results.length, offset, limit)) {
859
- break outer;
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
- // Apply aggregation if requested
865
- if (options.aggregation && results.length > 0) {
866
- const aggregated = this.aggregate(results, options.aggregation, options.groupBy, options.timeBucket);
867
- this.metricCache.set(cacheKey, aggregated);
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
- return aggregated;
1079
+ if (span)
1080
+ span.setAttribute('obs_toolkit.query.result_count', paginated.length);
1081
+ return paginated;
871
1082
  }
872
- const paginated = paginateResults(results, offset, limit);
873
- this.metricCache.set(cacheKey, paginated);
874
- this.circuitBreaker.recordSuccess();
875
- logTiming();
876
- return paginated;
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
- const timer = startTiming();
991
- const logTiming = () => {
992
- const durationMs = timer.end();
993
- if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
994
- console.warn(`[obs-toolkit] Slow query: queryLLMEvents took ${durationMs.toFixed(1)}ms`);
995
- }
996
- };
997
- // Check circuit breaker - fail fast if open
998
- if (!this.circuitBreaker.canRequest()) {
999
- logTiming();
1000
- return [];
1001
- }
1002
- // Check cache first
1003
- const cacheKey = makeCacheKey('llm-events', options);
1004
- const cached = this.llmEventCache.get(cacheKey);
1005
- if (cached) {
1006
- logTiming();
1007
- return cached;
1008
- }
1009
- const files = getFilesInRange(this.telemetryDir, /llm-events-\d{4}-\d{2}-\d{2}\.jsonl(\.gz)?$/, options.startDate, options.endDate);
1010
- const results = [];
1011
- const limit = options.limit || 100;
1012
- const offset = options.offset || 0;
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
- if (options.search) {
1043
- const searchLower = options.search.toLowerCase();
1044
- const attrStr = JSON.stringify(event.attributes).toLowerCase();
1045
- if (!attrStr.includes(searchLower) && !event.name.toLowerCase().includes(searchLower))
1046
- return false;
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
- return true;
1049
- };
1050
- try {
1051
- for (const file of files) {
1052
- // Try to use index for pre-filtering
1053
- const matchingLines = this.tryUseIndex(file, 'llm-events', indexOptions);
1054
- if (matchingLines !== null) {
1055
- // Use indexed query - read only matching lines
1056
- const rawRecords = await readLinesByNumber(file, matchingLines);
1057
- for (const event of rawRecords) {
1058
- if (!event.timestamp || !event.name)
1059
- continue;
1060
- if (!applyFilters(event))
1061
- continue;
1062
- results.push({
1063
- timestamp: event.timestamp,
1064
- name: event.name,
1065
- attributes: event.attributes,
1066
- });
1067
- if (hasReachedLimit(results.length, offset, limit)) {
1068
- const paginated = paginateResults(results, offset, limit);
1069
- this.llmEventCache.set(cacheKey, paginated);
1070
- this.circuitBreaker.recordSuccess();
1071
- logTiming();
1072
- return paginated;
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
- else {
1077
- // Fall back to full file scan
1078
- for await (const event of streamJsonl(file)) {
1079
- if (!event.timestamp || !event.name)
1080
- continue;
1081
- // Apply indexable filters (since no index was used)
1082
- if (options.eventName && !event.name.includes(options.eventName))
1083
- continue;
1084
- if (!applyFilters(event))
1085
- continue;
1086
- results.push({
1087
- timestamp: event.timestamp,
1088
- name: event.name,
1089
- attributes: event.attributes,
1090
- });
1091
- if (hasReachedLimit(results.length, offset, limit)) {
1092
- const paginated = paginateResults(results, offset, limit);
1093
- this.llmEventCache.set(cacheKey, paginated);
1094
- this.circuitBreaker.recordSuccess();
1095
- logTiming();
1096
- return paginated;
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
- const paginated = paginateResults(results, offset, limit);
1102
- this.llmEventCache.set(cacheKey, paginated);
1103
- this.circuitBreaker.recordSuccess();
1104
- logTiming();
1105
- return paginated;
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
- const timer = startTiming();
1115
- const logTiming = () => {
1116
- const durationMs = timer.end();
1117
- if (durationMs > SLOW_QUERY_THRESHOLD_MS) {
1118
- console.warn(`[obs-toolkit] Slow query: queryEvaluations took ${durationMs.toFixed(1)}ms`);
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
- // Check circuit breaker - fail fast if open
1122
- if (!this.circuitBreaker.canRequest()) {
1123
- logTiming();
1124
- return [];
1125
- }
1126
- // Check cache first
1127
- const cacheKey = makeCacheKey('evaluations', options);
1128
- const cached = this.evaluationCache.get(cacheKey);
1129
- if (cached) {
1130
- logTiming();
1131
- return cached;
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
- if (options.scoreMax !== undefined && evaluation.scoreValue !== undefined) {
1154
- if (evaluation.scoreValue > options.scoreMax)
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
- // Session filter
1158
- if (options.sessionId && evaluation.sessionId !== options.sessionId) {
1159
- return false;
1160
- }
1161
- // Evaluator type filter (not indexed, so always applied here)
1162
- if (options.evaluatorType && evaluation.evaluatorType !== options.evaluatorType) {
1163
- return false;
1164
- }
1165
- return true;
1166
- };
1167
- try {
1168
- for (const file of files) {
1169
- // Try to use index for pre-filtering
1170
- const matchingLines = this.tryUseIndex(file, 'evaluations', indexOptions);
1171
- if (matchingLines !== null) {
1172
- // Use indexed query - read only matching lines
1173
- const rawRecords = await readLinesByNumber(file, matchingLines);
1174
- for (const raw of rawRecords) {
1175
- const evaluation = normalizeEvaluation(raw);
1176
- if (!evaluation)
1177
- continue;
1178
- if (!applyFilters(evaluation))
1179
- continue;
1180
- results.push(evaluation);
1181
- if (hasReachedLimit(results.length, offset, limit)) {
1182
- const paginated = paginateResults(results, offset, limit);
1183
- this.evaluationCache.set(cacheKey, paginated);
1184
- this.circuitBreaker.recordSuccess();
1185
- logTiming();
1186
- return paginated;
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
- else {
1191
- // Fall back to full file scan
1192
- for await (const raw of streamJsonl(file)) {
1193
- const evaluation = normalizeEvaluation(raw);
1194
- if (!evaluation)
1195
- continue;
1196
- // Apply indexable filters (since no index was used)
1197
- if (options.traceId && evaluation.traceId !== options.traceId)
1198
- continue;
1199
- if (options.evaluationName && !evaluation.evaluationName.includes(options.evaluationName))
1200
- continue;
1201
- if (options.scoreLabel && evaluation.scoreLabel !== options.scoreLabel)
1202
- continue;
1203
- if (options.responseId && evaluation.responseId !== options.responseId)
1204
- continue;
1205
- if (options.evaluator && evaluation.evaluator !== options.evaluator)
1206
- continue;
1207
- if (!applyFilters(evaluation))
1208
- continue;
1209
- results.push(evaluation);
1210
- if (hasReachedLimit(results.length, offset, limit)) {
1211
- const paginated = paginateResults(results, offset, limit);
1212
- this.evaluationCache.set(cacheKey, paginated);
1213
- this.circuitBreaker.recordSuccess();
1214
- logTiming();
1215
- return paginated;
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
- const paginated = paginateResults(results, offset, limit);
1221
- this.evaluationCache.set(cacheKey, paginated);
1222
- this.circuitBreaker.recordSuccess();
1223
- logTiming();
1224
- return paginated;
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
  */