observability-toolkit 1.8.2 → 1.8.5

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 (259) hide show
  1. package/README.md +60 -0
  2. package/dist/backends/index.d.ts +43 -0
  3. package/dist/backends/index.d.ts.map +1 -1
  4. package/dist/backends/index.js +41 -0
  5. package/dist/backends/index.js.map +1 -1
  6. package/dist/backends/index.test.d.ts +5 -0
  7. package/dist/backends/index.test.d.ts.map +1 -0
  8. package/dist/backends/index.test.js +156 -0
  9. package/dist/backends/index.test.js.map +1 -0
  10. package/dist/backends/local-jsonl-boolean-search.test.js +15 -12
  11. package/dist/backends/local-jsonl-boolean-search.test.js.map +1 -1
  12. package/dist/backends/local-jsonl-cache.test.d.ts +2 -0
  13. package/dist/backends/local-jsonl-cache.test.d.ts.map +1 -0
  14. package/dist/backends/local-jsonl-cache.test.js +295 -0
  15. package/dist/backends/local-jsonl-cache.test.js.map +1 -0
  16. package/dist/backends/local-jsonl-circuit-breaker.test.d.ts +2 -0
  17. package/dist/backends/local-jsonl-circuit-breaker.test.d.ts.map +1 -0
  18. package/dist/backends/local-jsonl-circuit-breaker.test.js +180 -0
  19. package/dist/backends/local-jsonl-circuit-breaker.test.js.map +1 -0
  20. package/dist/backends/local-jsonl-export.test.d.ts +2 -0
  21. package/dist/backends/local-jsonl-export.test.d.ts.map +1 -0
  22. package/dist/backends/local-jsonl-export.test.js +704 -0
  23. package/dist/backends/local-jsonl-export.test.js.map +1 -0
  24. package/dist/backends/local-jsonl-index.test.d.ts +2 -0
  25. package/dist/backends/local-jsonl-index.test.d.ts.map +1 -0
  26. package/dist/backends/local-jsonl-index.test.js +554 -0
  27. package/dist/backends/local-jsonl-index.test.js.map +1 -0
  28. package/dist/backends/local-jsonl-logs.test.d.ts +2 -0
  29. package/dist/backends/local-jsonl-logs.test.d.ts.map +1 -0
  30. package/dist/backends/local-jsonl-logs.test.js +612 -0
  31. package/dist/backends/local-jsonl-logs.test.js.map +1 -0
  32. package/dist/backends/local-jsonl-metrics.test.d.ts +2 -0
  33. package/dist/backends/local-jsonl-metrics.test.d.ts.map +1 -0
  34. package/dist/backends/local-jsonl-metrics.test.js +876 -0
  35. package/dist/backends/local-jsonl-metrics.test.js.map +1 -0
  36. package/dist/backends/local-jsonl-traces.test.d.ts +2 -0
  37. package/dist/backends/local-jsonl-traces.test.d.ts.map +1 -0
  38. package/dist/backends/local-jsonl-traces.test.js +1729 -0
  39. package/dist/backends/local-jsonl-traces.test.js.map +1 -0
  40. package/dist/backends/local-jsonl.d.ts +9 -0
  41. package/dist/backends/local-jsonl.d.ts.map +1 -1
  42. package/dist/backends/local-jsonl.js +348 -227
  43. package/dist/backends/local-jsonl.js.map +1 -1
  44. package/dist/backends/local-jsonl.test.js +290 -21
  45. package/dist/backends/local-jsonl.test.js.map +1 -1
  46. package/dist/backends/signoz-api-circuit-breaker.test.d.ts +6 -0
  47. package/dist/backends/signoz-api-circuit-breaker.test.d.ts.map +1 -0
  48. package/dist/backends/signoz-api-circuit-breaker.test.js +548 -0
  49. package/dist/backends/signoz-api-circuit-breaker.test.js.map +1 -0
  50. package/dist/backends/signoz-api-rate-limiter.test.d.ts +6 -0
  51. package/dist/backends/signoz-api-rate-limiter.test.d.ts.map +1 -0
  52. package/dist/backends/signoz-api-rate-limiter.test.js +389 -0
  53. package/dist/backends/signoz-api-rate-limiter.test.js.map +1 -0
  54. package/dist/backends/signoz-api-ssrf.test.d.ts +6 -0
  55. package/dist/backends/signoz-api-ssrf.test.d.ts.map +1 -0
  56. package/dist/backends/signoz-api-ssrf.test.js +216 -0
  57. package/dist/backends/signoz-api-ssrf.test.js.map +1 -0
  58. package/dist/backends/signoz-api-test-helpers.d.ts +80 -0
  59. package/dist/backends/signoz-api-test-helpers.d.ts.map +1 -0
  60. package/dist/backends/signoz-api-test-helpers.js +79 -0
  61. package/dist/backends/signoz-api-test-helpers.js.map +1 -0
  62. package/dist/backends/signoz-api.d.ts +16 -0
  63. package/dist/backends/signoz-api.d.ts.map +1 -1
  64. package/dist/backends/signoz-api.js +71 -9
  65. package/dist/backends/signoz-api.js.map +1 -1
  66. package/dist/backends/signoz-api.test.d.ts +9 -0
  67. package/dist/backends/signoz-api.test.d.ts.map +1 -1
  68. package/dist/backends/signoz-api.test.js +14 -1027
  69. package/dist/backends/signoz-api.test.js.map +1 -1
  70. package/dist/lib/cache.d.ts +47 -1
  71. package/dist/lib/cache.d.ts.map +1 -1
  72. package/dist/lib/cache.js +40 -3
  73. package/dist/lib/cache.js.map +1 -1
  74. package/dist/lib/circuit-breaker.d.ts +83 -0
  75. package/dist/lib/circuit-breaker.d.ts.map +1 -0
  76. package/dist/lib/circuit-breaker.js +125 -0
  77. package/dist/lib/circuit-breaker.js.map +1 -0
  78. package/dist/lib/circuit-breaker.test.d.ts +2 -0
  79. package/dist/lib/circuit-breaker.test.d.ts.map +1 -0
  80. package/dist/lib/circuit-breaker.test.js +263 -0
  81. package/dist/lib/circuit-breaker.test.js.map +1 -0
  82. package/dist/lib/constants-symlink.test.d.ts +12 -0
  83. package/dist/lib/constants-symlink.test.d.ts.map +1 -0
  84. package/dist/lib/constants-symlink.test.js +357 -0
  85. package/dist/lib/constants-symlink.test.js.map +1 -0
  86. package/dist/lib/constants.d.ts +43 -0
  87. package/dist/lib/constants.d.ts.map +1 -1
  88. package/dist/lib/constants.js +154 -24
  89. package/dist/lib/constants.js.map +1 -1
  90. package/dist/lib/constants.test.js +156 -7
  91. package/dist/lib/constants.test.js.map +1 -1
  92. package/dist/lib/edge-cases.test.d.ts +11 -0
  93. package/dist/lib/edge-cases.test.d.ts.map +1 -0
  94. package/dist/lib/edge-cases.test.js +634 -0
  95. package/dist/lib/edge-cases.test.js.map +1 -0
  96. package/dist/lib/error-sanitizer.d.ts.map +1 -1
  97. package/dist/lib/error-sanitizer.js +62 -26
  98. package/dist/lib/error-sanitizer.js.map +1 -1
  99. package/dist/lib/error-sanitizer.test.js +186 -0
  100. package/dist/lib/error-sanitizer.test.js.map +1 -1
  101. package/dist/lib/error-types.d.ts +54 -0
  102. package/dist/lib/error-types.d.ts.map +1 -0
  103. package/dist/lib/error-types.js +154 -0
  104. package/dist/lib/error-types.js.map +1 -0
  105. package/dist/lib/error-types.test.d.ts +2 -0
  106. package/dist/lib/error-types.test.d.ts.map +1 -0
  107. package/dist/lib/error-types.test.js +196 -0
  108. package/dist/lib/error-types.test.js.map +1 -0
  109. package/dist/lib/file-utils.test.js +3 -3
  110. package/dist/lib/file-utils.test.js.map +1 -1
  111. package/dist/lib/indexer.test.js +157 -24
  112. package/dist/lib/indexer.test.js.map +1 -1
  113. package/dist/lib/input-validator.d.ts +17 -0
  114. package/dist/lib/input-validator.d.ts.map +1 -1
  115. package/dist/lib/input-validator.fuzz.test.d.ts +12 -0
  116. package/dist/lib/input-validator.fuzz.test.d.ts.map +1 -0
  117. package/dist/lib/input-validator.fuzz.test.js +290 -0
  118. package/dist/lib/input-validator.fuzz.test.js.map +1 -0
  119. package/dist/lib/input-validator.js +62 -3
  120. package/dist/lib/input-validator.js.map +1 -1
  121. package/dist/lib/input-validator.test.js +129 -1
  122. package/dist/lib/input-validator.test.js.map +1 -1
  123. package/dist/lib/logger.d.ts +46 -0
  124. package/dist/lib/logger.d.ts.map +1 -0
  125. package/dist/lib/logger.js +81 -0
  126. package/dist/lib/logger.js.map +1 -0
  127. package/dist/lib/logger.test.d.ts +2 -0
  128. package/dist/lib/logger.test.d.ts.map +1 -0
  129. package/dist/lib/logger.test.js +122 -0
  130. package/dist/lib/logger.test.js.map +1 -0
  131. package/dist/lib/query-sanitizer.d.ts +51 -3
  132. package/dist/lib/query-sanitizer.d.ts.map +1 -1
  133. package/dist/lib/query-sanitizer.js +105 -31
  134. package/dist/lib/query-sanitizer.js.map +1 -1
  135. package/dist/lib/query-sanitizer.test.js +102 -1
  136. package/dist/lib/query-sanitizer.test.js.map +1 -1
  137. package/dist/lib/server-utils.d.ts +88 -0
  138. package/dist/lib/server-utils.d.ts.map +1 -0
  139. package/dist/lib/server-utils.js +173 -0
  140. package/dist/lib/server-utils.js.map +1 -0
  141. package/dist/lib/shared-schemas.d.ts +81 -0
  142. package/dist/lib/shared-schemas.d.ts.map +1 -0
  143. package/dist/lib/shared-schemas.js +80 -0
  144. package/dist/lib/shared-schemas.js.map +1 -0
  145. package/dist/lib/shared-schemas.test.d.ts +5 -0
  146. package/dist/lib/shared-schemas.test.d.ts.map +1 -0
  147. package/dist/lib/shared-schemas.test.js +106 -0
  148. package/dist/lib/shared-schemas.test.js.map +1 -0
  149. package/dist/lib/toon-encoder.d.ts +26 -0
  150. package/dist/lib/toon-encoder.d.ts.map +1 -0
  151. package/dist/lib/toon-encoder.js +61 -0
  152. package/dist/lib/toon-encoder.js.map +1 -0
  153. package/dist/lib/toon-encoder.test.d.ts +5 -0
  154. package/dist/lib/toon-encoder.test.d.ts.map +1 -0
  155. package/dist/lib/toon-encoder.test.js +85 -0
  156. package/dist/lib/toon-encoder.test.js.map +1 -0
  157. package/dist/server.d.ts +1 -49
  158. package/dist/server.d.ts.map +1 -1
  159. package/dist/server.js +154 -162
  160. package/dist/server.js.map +1 -1
  161. package/dist/server.test.js +198 -7
  162. package/dist/server.test.js.map +1 -1
  163. package/dist/test-helpers/env-utils.d.ts +87 -0
  164. package/dist/test-helpers/env-utils.d.ts.map +1 -0
  165. package/dist/test-helpers/env-utils.js +132 -0
  166. package/dist/test-helpers/env-utils.js.map +1 -0
  167. package/dist/test-helpers/file-utils.d.ts +67 -0
  168. package/dist/test-helpers/file-utils.d.ts.map +1 -1
  169. package/dist/test-helpers/file-utils.js +165 -2
  170. package/dist/test-helpers/file-utils.js.map +1 -1
  171. package/dist/test-helpers/fuzz-generators.d.ts +58 -0
  172. package/dist/test-helpers/fuzz-generators.d.ts.map +1 -0
  173. package/dist/test-helpers/fuzz-generators.js +216 -0
  174. package/dist/test-helpers/fuzz-generators.js.map +1 -0
  175. package/dist/test-helpers/index.d.ts +11 -0
  176. package/dist/test-helpers/index.d.ts.map +1 -0
  177. package/dist/test-helpers/index.js +30 -0
  178. package/dist/test-helpers/index.js.map +1 -0
  179. package/dist/test-helpers/memfs-utils.d.ts +181 -0
  180. package/dist/test-helpers/memfs-utils.d.ts.map +1 -0
  181. package/dist/test-helpers/memfs-utils.js +292 -0
  182. package/dist/test-helpers/memfs-utils.js.map +1 -0
  183. package/dist/test-helpers/memfs-utils.test.d.ts +5 -0
  184. package/dist/test-helpers/memfs-utils.test.d.ts.map +1 -0
  185. package/dist/test-helpers/memfs-utils.test.js +338 -0
  186. package/dist/test-helpers/memfs-utils.test.js.map +1 -0
  187. package/dist/test-helpers/mock-backends.d.ts +113 -2
  188. package/dist/test-helpers/mock-backends.d.ts.map +1 -1
  189. package/dist/test-helpers/mock-backends.js +199 -3
  190. package/dist/test-helpers/mock-backends.js.map +1 -1
  191. package/dist/test-helpers/mock-backends.test.d.ts +5 -0
  192. package/dist/test-helpers/mock-backends.test.d.ts.map +1 -0
  193. package/dist/test-helpers/mock-backends.test.js +368 -0
  194. package/dist/test-helpers/mock-backends.test.js.map +1 -0
  195. package/dist/test-helpers/race-condition-helpers.d.ts +85 -0
  196. package/dist/test-helpers/race-condition-helpers.d.ts.map +1 -0
  197. package/dist/test-helpers/race-condition-helpers.js +279 -0
  198. package/dist/test-helpers/race-condition-helpers.js.map +1 -0
  199. package/dist/test-helpers/schema-validators.d.ts +32 -0
  200. package/dist/test-helpers/schema-validators.d.ts.map +1 -0
  201. package/dist/test-helpers/schema-validators.js +125 -0
  202. package/dist/test-helpers/schema-validators.js.map +1 -0
  203. package/dist/test-helpers/test-data-builders.d.ts +260 -0
  204. package/dist/test-helpers/test-data-builders.d.ts.map +1 -0
  205. package/dist/test-helpers/test-data-builders.js +337 -0
  206. package/dist/test-helpers/test-data-builders.js.map +1 -0
  207. package/dist/test-helpers/test-data-builders.test.d.ts +2 -0
  208. package/dist/test-helpers/test-data-builders.test.d.ts.map +1 -0
  209. package/dist/test-helpers/test-data-builders.test.js +306 -0
  210. package/dist/test-helpers/test-data-builders.test.js.map +1 -0
  211. package/dist/test-helpers/tool-validators.d.ts +28 -0
  212. package/dist/test-helpers/tool-validators.d.ts.map +1 -0
  213. package/dist/test-helpers/tool-validators.js +71 -0
  214. package/dist/test-helpers/tool-validators.js.map +1 -0
  215. package/dist/tools/context-stats.d.ts +1 -0
  216. package/dist/tools/context-stats.d.ts.map +1 -1
  217. package/dist/tools/context-stats.js +9 -5
  218. package/dist/tools/context-stats.js.map +1 -1
  219. package/dist/tools/context-stats.test.js +24 -10
  220. package/dist/tools/context-stats.test.js.map +1 -1
  221. package/dist/tools/get-trace-url.js +2 -2
  222. package/dist/tools/get-trace-url.js.map +1 -1
  223. package/dist/tools/health-check.js +2 -2
  224. package/dist/tools/health-check.js.map +1 -1
  225. package/dist/tools/query-evaluations.d.ts +21 -18
  226. package/dist/tools/query-evaluations.d.ts.map +1 -1
  227. package/dist/tools/query-evaluations.js +33 -19
  228. package/dist/tools/query-evaluations.js.map +1 -1
  229. package/dist/tools/query-evaluations.test.js +60 -63
  230. package/dist/tools/query-evaluations.test.js.map +1 -1
  231. package/dist/tools/query-llm-events.d.ts +19 -15
  232. package/dist/tools/query-llm-events.d.ts.map +1 -1
  233. package/dist/tools/query-llm-events.js +31 -15
  234. package/dist/tools/query-llm-events.js.map +1 -1
  235. package/dist/tools/query-llm-events.test.js +277 -12
  236. package/dist/tools/query-llm-events.test.js.map +1 -1
  237. package/dist/tools/query-logs.d.ts +22 -22
  238. package/dist/tools/query-logs.d.ts.map +1 -1
  239. package/dist/tools/query-logs.js +9 -9
  240. package/dist/tools/query-logs.js.map +1 -1
  241. package/dist/tools/query-logs.test.js +19 -72
  242. package/dist/tools/query-logs.test.js.map +1 -1
  243. package/dist/tools/query-metrics.d.ts +14 -14
  244. package/dist/tools/query-metrics.d.ts.map +1 -1
  245. package/dist/tools/query-metrics.js +9 -9
  246. package/dist/tools/query-metrics.js.map +1 -1
  247. package/dist/tools/query-metrics.test.js +12 -25
  248. package/dist/tools/query-metrics.test.js.map +1 -1
  249. package/dist/tools/query-traces.d.ts +28 -28
  250. package/dist/tools/query-traces.d.ts.map +1 -1
  251. package/dist/tools/query-traces.js +18 -18
  252. package/dist/tools/query-traces.js.map +1 -1
  253. package/dist/tools/query-traces.test.js +58 -54
  254. package/dist/tools/query-traces.test.js.map +1 -1
  255. package/dist/tools/setup-claudeignore.js +7 -7
  256. package/dist/tools/setup-claudeignore.js.map +1 -1
  257. package/dist/tools/setup-claudeignore.test.js +4 -25
  258. package/dist/tools/setup-claudeignore.test.js.map +1 -1
  259. package/package.json +4 -2
@@ -12,6 +12,7 @@ import { listFiles, streamJsonl, parseDateFromFilename, getDateString, paginateR
12
12
  import { QueryCache, makeCacheKey } from '../lib/cache.js';
13
13
  import { getIndexPath, readIndex, isIndexStale, queryIndex, readLinesByNumber, } from '../lib/indexer.js';
14
14
  import { sanitizePath } from '../lib/error-sanitizer.js';
15
+ import { CircuitBreaker } from '../lib/circuit-breaker.js';
15
16
  import { existsSync } from 'fs';
16
17
  function startTiming() {
17
18
  const start = performance.now();
@@ -20,6 +21,38 @@ function startTiming() {
20
21
  };
21
22
  }
22
23
  const SLOW_QUERY_THRESHOLD_MS = 500;
24
+ /**
25
+ * LRU regex cache to avoid recompiling frequently-used patterns.
26
+ * Maximum 100 patterns cached; uses Map insertion order for LRU eviction.
27
+ */
28
+ const regexCache = new Map();
29
+ const MAX_CACHED_REGEX_PATTERNS = 100;
30
+ /**
31
+ * Get a cached regex or compile and cache a new one.
32
+ * Returns null for invalid patterns (logs warning).
33
+ */
34
+ function getCachedRegex(pattern) {
35
+ const cached = regexCache.get(pattern);
36
+ if (cached) {
37
+ return cached;
38
+ }
39
+ try {
40
+ const regex = new RegExp(pattern);
41
+ // LRU eviction: remove oldest entry if at capacity
42
+ if (regexCache.size >= MAX_CACHED_REGEX_PATTERNS) {
43
+ const firstKey = regexCache.keys().next().value;
44
+ if (firstKey !== undefined) {
45
+ regexCache.delete(firstKey);
46
+ }
47
+ }
48
+ regexCache.set(pattern, regex);
49
+ return regex;
50
+ }
51
+ catch {
52
+ console.warn(`Invalid regex pattern: ${pattern}`);
53
+ return null;
54
+ }
55
+ }
23
56
  /**
24
57
  * Insert item into a sorted array, maintaining sort order and max size.
25
58
  * More efficient than sort+slice for top-K selection (O(n) vs O(n log n)).
@@ -29,6 +62,8 @@ function insertSortedBounded(arr, item, maxSize, compareFn) {
29
62
  let low = 0;
30
63
  let high = arr.length;
31
64
  while (low < high) {
65
+ // Use unsigned right shift (>>>) to compute midpoint without integer overflow
66
+ // when (low + high) exceeds Number.MAX_SAFE_INTEGER
32
67
  const mid = (low + high) >>> 1;
33
68
  if (compareFn(arr[mid], item) <= 0) {
34
69
  low = mid + 1;
@@ -431,9 +466,23 @@ export class LocalJsonlBackend {
431
466
  llmEventCache = new QueryCache();
432
467
  evaluationCache = new QueryCache();
433
468
  useIndexes;
469
+ circuitBreaker;
434
470
  constructor(telemetryDir, useIndexes = true) {
435
471
  this.telemetryDir = telemetryDir || TELEMETRY_DIR;
436
472
  this.useIndexes = useIndexes;
473
+ this.circuitBreaker = new CircuitBreaker({ name: 'local-file-io' });
474
+ }
475
+ /**
476
+ * Get circuit breaker state (for health check and testing)
477
+ */
478
+ getCircuitBreakerState() {
479
+ return this.circuitBreaker.getState();
480
+ }
481
+ /**
482
+ * Reset circuit breaker (for testing)
483
+ */
484
+ resetCircuitBreaker() {
485
+ this.circuitBreaker.reset();
437
486
  }
438
487
  /**
439
488
  * Clear all query caches
@@ -479,6 +528,11 @@ export class LocalJsonlBackend {
479
528
  console.warn(`[obs-toolkit] Slow query: queryTraces took ${durationMs.toFixed(1)}ms`);
480
529
  }
481
530
  };
531
+ // Check circuit breaker - fail fast if open
532
+ if (!this.circuitBreaker.canRequest()) {
533
+ logTiming();
534
+ return [];
535
+ }
482
536
  // Check cache first
483
537
  const cacheKey = makeCacheKey('traces', options);
484
538
  const cached = this.traceCache.get(cacheKey);
@@ -490,16 +544,10 @@ export class LocalJsonlBackend {
490
544
  const results = [];
491
545
  const limit = options.limit || 100;
492
546
  const offset = options.offset || 0;
493
- // Compile regex once outside the loop, handling invalid patterns gracefully
494
- let spanNameRegex = null;
495
- if (options.spanNameRegex) {
496
- try {
497
- spanNameRegex = new RegExp(options.spanNameRegex);
498
- }
499
- catch {
500
- console.warn(`Invalid spanNameRegex pattern: ${options.spanNameRegex}`);
501
- }
502
- }
547
+ // Use cached regex to avoid recompilation for frequently-used patterns
548
+ const spanNameRegex = options.spanNameRegex
549
+ ? getCachedRegex(options.spanNameRegex)
550
+ : null;
503
551
  // Build index query options for indexable filters
504
552
  const indexOptions = {
505
553
  traceId: options.traceId,
@@ -558,59 +606,69 @@ export class LocalJsonlBackend {
558
606
  return false;
559
607
  return true;
560
608
  };
561
- for (const file of files) {
562
- // Try to use index for pre-filtering
563
- const matchingLines = this.tryUseIndex(file, 'traces', indexOptions);
564
- if (matchingLines !== null) {
565
- // Use indexed query - read only matching lines
566
- const rawRecords = await readLinesByNumber(file, matchingLines);
567
- for (const raw of rawRecords) {
568
- const span = normalizeSpan(raw);
569
- if (!span)
570
- continue;
571
- if (!applyFilters(span))
572
- continue;
573
- results.push(span);
574
- if (hasReachedLimit(results.length, offset, limit)) {
575
- const paginated = paginateResults(results, offset, limit);
576
- this.traceCache.set(cacheKey, paginated);
577
- logTiming();
578
- return paginated;
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;
629
+ }
579
630
  }
580
631
  }
581
- }
582
- else {
583
- // Fall back to full file scan
584
- for await (const raw of streamJsonl(file)) {
585
- const span = normalizeSpan(raw);
586
- if (!span)
587
- continue;
588
- // Apply indexable filters (since no index was used)
589
- if (options.traceId && span.traceId !== options.traceId)
590
- continue;
591
- if (options.spanName && !span.name.includes(options.spanName))
592
- continue;
593
- if (options.serviceName) {
594
- const svc = span.attributes?.['service.name'];
595
- if (svc !== options.serviceName)
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)
596
637
  continue;
597
- }
598
- if (!applyFilters(span))
599
- continue;
600
- results.push(span);
601
- if (hasReachedLimit(results.length, offset, limit)) {
602
- const paginated = paginateResults(results, offset, limit);
603
- this.traceCache.set(cacheKey, paginated);
604
- logTiming();
605
- return paginated;
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)
646
+ 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;
657
+ }
606
658
  }
607
659
  }
608
660
  }
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;
609
671
  }
610
- const paginated = paginateResults(results, offset, limit);
611
- this.traceCache.set(cacheKey, paginated);
612
- logTiming();
613
- return paginated;
614
672
  }
615
673
  async queryLogs(options) {
616
674
  const timer = startTiming();
@@ -620,6 +678,11 @@ export class LocalJsonlBackend {
620
678
  console.warn(`[obs-toolkit] Slow query: queryLogs took ${durationMs.toFixed(1)}ms`);
621
679
  }
622
680
  };
681
+ // Check circuit breaker - fail fast if open
682
+ if (!this.circuitBreaker.canRequest()) {
683
+ logTiming();
684
+ return [];
685
+ }
623
686
  // Check cache first
624
687
  const cacheKey = makeCacheKey('logs', options);
625
688
  const cached = this.logCache.get(cacheKey);
@@ -678,54 +741,64 @@ export class LocalJsonlBackend {
678
741
  }
679
742
  return true;
680
743
  };
681
- for (const file of files) {
682
- // Try to use index for pre-filtering
683
- const matchingLines = this.tryUseIndex(file, 'logs', indexOptions);
684
- if (matchingLines !== null) {
685
- // Use indexed query - read only matching lines
686
- const rawRecords = await readLinesByNumber(file, matchingLines);
687
- for (const raw of rawRecords) {
688
- const log = normalizeLog(raw, options.extractFields);
689
- if (!log)
690
- continue;
691
- if (!applyFilters(log))
692
- continue;
693
- results.push(log);
694
- if (hasReachedLimit(results.length, offset, limit)) {
695
- const paginated = paginateResults(results, offset, limit);
696
- this.logCache.set(cacheKey, paginated);
697
- logTiming();
698
- return paginated;
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
+ }
699
765
  }
700
766
  }
701
- }
702
- else {
703
- // Fall back to full file scan
704
- for await (const raw of streamJsonl(file)) {
705
- const log = normalizeLog(raw, options.extractFields);
706
- if (!log)
707
- continue;
708
- // Apply indexable filters (since no index was used)
709
- if (options.severity && log.severity.toUpperCase() !== options.severity.toUpperCase())
710
- continue;
711
- if (options.traceId && log.traceId !== options.traceId)
712
- continue;
713
- if (!applyFilters(log))
714
- continue;
715
- results.push(log);
716
- if (hasReachedLimit(results.length, offset, limit)) {
717
- const paginated = paginateResults(results, offset, limit);
718
- this.logCache.set(cacheKey, paginated);
719
- logTiming();
720
- return paginated;
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;
787
+ }
721
788
  }
722
789
  }
723
790
  }
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;
724
801
  }
725
- const paginated = paginateResults(results, offset, limit);
726
- this.logCache.set(cacheKey, paginated);
727
- logTiming();
728
- return paginated;
729
802
  }
730
803
  async queryMetrics(options) {
731
804
  const timer = startTiming();
@@ -735,6 +808,11 @@ export class LocalJsonlBackend {
735
808
  console.warn(`[obs-toolkit] Slow query: queryMetrics took ${durationMs.toFixed(1)}ms`);
736
809
  }
737
810
  };
811
+ // Check circuit breaker - fail fast if open
812
+ if (!this.circuitBreaker.canRequest()) {
813
+ logTiming();
814
+ return [];
815
+ }
738
816
  // Check cache first
739
817
  const cacheKey = makeCacheKey('metrics', options);
740
818
  const cached = this.metricCache.get(cacheKey);
@@ -750,49 +828,58 @@ export class LocalJsonlBackend {
750
828
  const indexOptions = {
751
829
  metricName: options.metricName,
752
830
  };
753
- outer: for (const file of files) {
754
- // Try to use index for pre-filtering
755
- const matchingLines = this.tryUseIndex(file, 'metrics', indexOptions);
756
- if (matchingLines !== null) {
757
- // Use indexed query - read only matching lines
758
- const rawRecords = await readLinesByNumber(file, matchingLines);
759
- for (const raw of rawRecords) {
760
- const point = normalizeMetric(raw);
761
- if (!point)
762
- continue;
763
- results.push(point);
764
- if (hasReachedLimit(results.length, offset, limit)) {
765
- break outer;
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;
845
+ }
766
846
  }
767
847
  }
768
- }
769
- else {
770
- // Fall back to full file scan
771
- for await (const raw of streamJsonl(file)) {
772
- const point = normalizeMetric(raw);
773
- if (!point)
774
- continue;
775
- // Apply filters (since no index was used)
776
- if (options.metricName && !point.name.includes(options.metricName))
777
- continue;
778
- results.push(point);
779
- if (hasReachedLimit(results.length, offset, limit)) {
780
- break outer;
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;
860
+ }
781
861
  }
782
862
  }
783
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);
868
+ this.circuitBreaker.recordSuccess();
869
+ logTiming();
870
+ return aggregated;
871
+ }
872
+ const paginated = paginateResults(results, offset, limit);
873
+ this.metricCache.set(cacheKey, paginated);
874
+ this.circuitBreaker.recordSuccess();
875
+ logTiming();
876
+ return paginated;
784
877
  }
785
- // Apply aggregation if requested
786
- if (options.aggregation && results.length > 0) {
787
- const aggregated = this.aggregate(results, options.aggregation, options.groupBy, options.timeBucket);
788
- this.metricCache.set(cacheKey, aggregated);
878
+ catch (error) {
879
+ this.circuitBreaker.recordFailure();
789
880
  logTiming();
790
- return aggregated;
881
+ throw error;
791
882
  }
792
- const paginated = paginateResults(results, offset, limit);
793
- this.metricCache.set(cacheKey, paginated);
794
- logTiming();
795
- return paginated;
796
883
  }
797
884
  aggregate(points, aggregation, groupBy, timeBucket) {
798
885
  // Parse time bucket if provided
@@ -907,6 +994,11 @@ export class LocalJsonlBackend {
907
994
  console.warn(`[obs-toolkit] Slow query: queryLLMEvents took ${durationMs.toFixed(1)}ms`);
908
995
  }
909
996
  };
997
+ // Check circuit breaker - fail fast if open
998
+ if (!this.circuitBreaker.canRequest()) {
999
+ logTiming();
1000
+ return [];
1001
+ }
910
1002
  // Check cache first
911
1003
  const cacheKey = makeCacheKey('llm-events', options);
912
1004
  const cached = this.llmEventCache.get(cacheKey);
@@ -955,58 +1047,68 @@ export class LocalJsonlBackend {
955
1047
  }
956
1048
  return true;
957
1049
  };
958
- for (const file of files) {
959
- // Try to use index for pre-filtering
960
- const matchingLines = this.tryUseIndex(file, 'llm-events', indexOptions);
961
- if (matchingLines !== null) {
962
- // Use indexed query - read only matching lines
963
- const rawRecords = await readLinesByNumber(file, matchingLines);
964
- for (const event of rawRecords) {
965
- if (!event.timestamp || !event.name)
966
- continue;
967
- if (!applyFilters(event))
968
- continue;
969
- results.push({
970
- timestamp: event.timestamp,
971
- name: event.name,
972
- attributes: event.attributes,
973
- });
974
- if (hasReachedLimit(results.length, offset, limit)) {
975
- const paginated = paginateResults(results, offset, limit);
976
- this.llmEventCache.set(cacheKey, paginated);
977
- logTiming();
978
- return paginated;
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;
1073
+ }
979
1074
  }
980
1075
  }
981
- }
982
- else {
983
- // Fall back to full file scan
984
- for await (const event of streamJsonl(file)) {
985
- if (!event.timestamp || !event.name)
986
- continue;
987
- // Apply indexable filters (since no index was used)
988
- if (options.eventName && !event.name.includes(options.eventName))
989
- continue;
990
- if (!applyFilters(event))
991
- continue;
992
- results.push({
993
- timestamp: event.timestamp,
994
- name: event.name,
995
- attributes: event.attributes,
996
- });
997
- if (hasReachedLimit(results.length, offset, limit)) {
998
- const paginated = paginateResults(results, offset, limit);
999
- this.llmEventCache.set(cacheKey, paginated);
1000
- logTiming();
1001
- return paginated;
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;
1097
+ }
1002
1098
  }
1003
1099
  }
1004
1100
  }
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;
1005
1111
  }
1006
- const paginated = paginateResults(results, offset, limit);
1007
- this.llmEventCache.set(cacheKey, paginated);
1008
- logTiming();
1009
- return paginated;
1010
1112
  }
1011
1113
  async queryEvaluations(options) {
1012
1114
  const timer = startTiming();
@@ -1016,6 +1118,11 @@ export class LocalJsonlBackend {
1016
1118
  console.warn(`[obs-toolkit] Slow query: queryEvaluations took ${durationMs.toFixed(1)}ms`);
1017
1119
  }
1018
1120
  };
1121
+ // Check circuit breaker - fail fast if open
1122
+ if (!this.circuitBreaker.canRequest()) {
1123
+ logTiming();
1124
+ return [];
1125
+ }
1019
1126
  // Check cache first
1020
1127
  const cacheKey = makeCacheKey('evaluations', options);
1021
1128
  const cached = this.evaluationCache.get(cacheKey);
@@ -1057,62 +1164,76 @@ export class LocalJsonlBackend {
1057
1164
  }
1058
1165
  return true;
1059
1166
  };
1060
- for (const file of files) {
1061
- // Try to use index for pre-filtering
1062
- const matchingLines = this.tryUseIndex(file, 'evaluations', indexOptions);
1063
- if (matchingLines !== null) {
1064
- // Use indexed query - read only matching lines
1065
- const rawRecords = await readLinesByNumber(file, matchingLines);
1066
- for (const raw of rawRecords) {
1067
- const evaluation = normalizeEvaluation(raw);
1068
- if (!evaluation)
1069
- continue;
1070
- if (!applyFilters(evaluation))
1071
- continue;
1072
- results.push(evaluation);
1073
- if (hasReachedLimit(results.length, offset, limit)) {
1074
- const paginated = paginateResults(results, offset, limit);
1075
- this.evaluationCache.set(cacheKey, paginated);
1076
- logTiming();
1077
- return paginated;
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;
1187
+ }
1078
1188
  }
1079
1189
  }
1080
- }
1081
- else {
1082
- // Fall back to full file scan
1083
- for await (const raw of streamJsonl(file)) {
1084
- const evaluation = normalizeEvaluation(raw);
1085
- if (!evaluation)
1086
- continue;
1087
- // Apply indexable filters (since no index was used)
1088
- if (options.traceId && evaluation.traceId !== options.traceId)
1089
- continue;
1090
- if (options.evaluationName && !evaluation.evaluationName.includes(options.evaluationName))
1091
- continue;
1092
- if (options.scoreLabel && evaluation.scoreLabel !== options.scoreLabel)
1093
- continue;
1094
- if (options.responseId && evaluation.responseId !== options.responseId)
1095
- continue;
1096
- if (options.evaluator && evaluation.evaluator !== options.evaluator)
1097
- continue;
1098
- if (!applyFilters(evaluation))
1099
- continue;
1100
- results.push(evaluation);
1101
- if (hasReachedLimit(results.length, offset, limit)) {
1102
- const paginated = paginateResults(results, offset, limit);
1103
- this.evaluationCache.set(cacheKey, paginated);
1104
- logTiming();
1105
- return paginated;
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;
1216
+ }
1106
1217
  }
1107
1218
  }
1108
1219
  }
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;
1109
1230
  }
1110
- const paginated = paginateResults(results, offset, limit);
1111
- this.evaluationCache.set(cacheKey, paginated);
1112
- logTiming();
1113
- return paginated;
1114
1231
  }
1115
1232
  async healthCheck() {
1233
+ // Check circuit breaker state
1234
+ if (this.circuitBreaker.getState() === 'open') {
1235
+ return { status: 'error', message: 'Circuit breaker open - file I/O temporarily unavailable' };
1236
+ }
1116
1237
  if (!existsSync(this.telemetryDir)) {
1117
1238
  // Security: sanitize path to prevent information disclosure
1118
1239
  return { status: 'error', message: `Telemetry directory not found: ${sanitizePath(this.telemetryDir)}` };