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
@@ -0,0 +1,1729 @@
1
+ import { describe, it, before, after, beforeEach } from 'node:test';
2
+ import * as assert from 'node:assert';
3
+ import * as path from 'path';
4
+ import { LocalJsonlBackend } from './local-jsonl.js';
5
+ import { getSharedTempDir, clearTempDir, removeSharedTempDir, writeJsonlFileAsync, getTestDate } from '../test-helpers/file-utils.js';
6
+ import { createMockSpan, createMockSpans, createDbSpan, resetBuilderCounters, } from '../test-helpers/test-data-builders.js';
7
+ describe('LocalJsonlBackend', () => {
8
+ let tempDir;
9
+ let backend;
10
+ before(() => {
11
+ tempDir = getSharedTempDir('LocalJsonlBackend-Traces');
12
+ });
13
+ beforeEach(() => {
14
+ clearTempDir(tempDir);
15
+ backend = new LocalJsonlBackend(tempDir);
16
+ });
17
+ after(() => {
18
+ removeSharedTempDir('LocalJsonlBackend-Traces');
19
+ });
20
+ describe('queryTraces', () => {
21
+ it('should read and normalize trace spans from JSONL files', async () => {
22
+ const today = getTestDate();
23
+ const mockSpans = [
24
+ {
25
+ traceId: 'trace1',
26
+ spanId: 'span1',
27
+ name: 'test-operation',
28
+ startTime: [1700000000, 0],
29
+ endTime: [1700000001, 500000000],
30
+ resource: { serviceName: 'test-service', serviceVersion: '1.0.0' },
31
+ attributes: { 'custom.attr': 'value1' },
32
+ },
33
+ ];
34
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
35
+ const results = await backend.queryTraces({});
36
+ assert.strictEqual(results.length, 1);
37
+ assert.strictEqual(results[0].traceId, 'trace1');
38
+ assert.strictEqual(results[0].spanId, 'span1');
39
+ assert.strictEqual(results[0].name, 'test-operation');
40
+ assert.strictEqual(results[0].attributes?.['service.name'], 'test-service');
41
+ assert.strictEqual(results[0].attributes?.['service.version'], '1.0.0');
42
+ assert.strictEqual(results[0].attributes?.['custom.attr'], 'value1');
43
+ });
44
+ it('should filter spans by traceId', async () => {
45
+ const today = getTestDate();
46
+ resetBuilderCounters();
47
+ // Use createMockSpan with explicit traceId overrides
48
+ const mockSpans = [
49
+ createMockSpan({ traceId: 'trace1', spanId: 'span1', name: 'op1' }),
50
+ createMockSpan({ traceId: 'trace2', spanId: 'span2', name: 'op2' }),
51
+ createMockSpan({ traceId: 'trace1', spanId: 'span3', name: 'op3' }),
52
+ ];
53
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
54
+ const results = await backend.queryTraces({ traceId: 'trace1' });
55
+ assert.strictEqual(results.length, 2);
56
+ assert.ok(results.every(s => s.traceId === 'trace1'));
57
+ });
58
+ it('should filter spans by spanName substring', async () => {
59
+ const today = getTestDate();
60
+ resetBuilderCounters();
61
+ // Use createMockSpan for simple spans with specific names
62
+ const mockSpans = [
63
+ createMockSpan({ traceId: 'trace1', name: 'user-create' }),
64
+ createMockSpan({ traceId: 'trace1', name: 'user-update' }),
65
+ createDbSpan('query', 'postgres', { traceId: 'trace1' }),
66
+ ];
67
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
68
+ const results = await backend.queryTraces({ spanName: 'user' });
69
+ assert.strictEqual(results.length, 2);
70
+ assert.ok(results.every(s => s.name.includes('user')));
71
+ });
72
+ it('should filter spans by duration range', async () => {
73
+ const today = getTestDate();
74
+ const mockSpans = [
75
+ {
76
+ traceId: 'trace1',
77
+ spanId: 'span1',
78
+ name: 'fast-op',
79
+ startTime: [1700000000, 0],
80
+ endTime: [1700000000, 500000000], // 0.5s
81
+ },
82
+ {
83
+ traceId: 'trace1',
84
+ spanId: 'span2',
85
+ name: 'medium-op',
86
+ startTime: [1700000000, 0],
87
+ endTime: [1700000002, 0], // 2s
88
+ },
89
+ {
90
+ traceId: 'trace1',
91
+ spanId: 'span3',
92
+ name: 'slow-op',
93
+ startTime: [1700000000, 0],
94
+ endTime: [1700000010, 0], // 10s
95
+ },
96
+ ];
97
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
98
+ const results = await backend.queryTraces({ minDurationMs: 1000, maxDurationMs: 5000 });
99
+ assert.strictEqual(results.length, 1);
100
+ assert.strictEqual(results[0].name, 'medium-op');
101
+ });
102
+ it('should filter spans by serviceName', async () => {
103
+ const today = getTestDate();
104
+ const mockSpans = [
105
+ {
106
+ traceId: 'trace1',
107
+ spanId: 'span1',
108
+ name: 'op1',
109
+ startTime: [1700000000, 0],
110
+ resource: { serviceName: 'service-a' },
111
+ },
112
+ {
113
+ traceId: 'trace1',
114
+ spanId: 'span2',
115
+ name: 'op2',
116
+ startTime: [1700000000, 0],
117
+ resource: { serviceName: 'service-b' },
118
+ },
119
+ ];
120
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
121
+ const results = await backend.queryTraces({ serviceName: 'service-a' });
122
+ assert.strictEqual(results.length, 1);
123
+ assert.strictEqual(results[0].attributes?.['service.name'], 'service-a');
124
+ });
125
+ it('should apply limit and offset to results', async () => {
126
+ const today = getTestDate();
127
+ resetBuilderCounters();
128
+ // Use createMockSpans with dynamic overrides for indexed data
129
+ const mockSpans = createMockSpans(150, (i) => ({
130
+ traceId: `trace${i}`,
131
+ spanId: `span${i}`,
132
+ name: `op${i}`,
133
+ }));
134
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
135
+ const results = await backend.queryTraces({ limit: 50, offset: 25 });
136
+ assert.strictEqual(results.length, 50);
137
+ assert.strictEqual(results[0].traceId, 'trace25');
138
+ });
139
+ it('should skip invalid spans (missing required fields)', async () => {
140
+ const today = getTestDate();
141
+ const mockSpans = [
142
+ { traceId: 'trace1', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
143
+ { traceId: 'trace2', spanId: 'span2', startTime: [1700000000, 0] }, // missing name
144
+ { spanId: 'span3', name: 'op3', startTime: [1700000000, 0] }, // missing traceId
145
+ { traceId: 'trace4', name: 'op4', startTime: [1700000000, 0] }, // missing spanId
146
+ ];
147
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
148
+ const results = await backend.queryTraces({});
149
+ assert.strictEqual(results.length, 1);
150
+ assert.strictEqual(results[0].traceId, 'trace1');
151
+ });
152
+ it('should convert duration from [seconds, nanoseconds] array', async () => {
153
+ const today = getTestDate();
154
+ const mockSpans = [
155
+ {
156
+ traceId: 'trace1',
157
+ spanId: 'span1',
158
+ name: 'op1',
159
+ startTime: [1700000000, 0],
160
+ duration: [2, 500000000], // 2.5 seconds
161
+ },
162
+ ];
163
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
164
+ const results = await backend.queryTraces({});
165
+ assert.strictEqual(results.length, 1);
166
+ assert.strictEqual(results[0].durationMs, 2500);
167
+ });
168
+ it('should convert span kind number to string', async () => {
169
+ const today = getTestDate();
170
+ const mockSpans = [
171
+ { traceId: 'trace1', spanId: 'span1', name: 'op1', kind: 0, startTime: [1700000000, 0] },
172
+ { traceId: 'trace2', spanId: 'span2', name: 'op2', kind: 1, startTime: [1700000000, 0] },
173
+ { traceId: 'trace3', spanId: 'span3', name: 'op3', kind: 2, startTime: [1700000000, 0] },
174
+ ];
175
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
176
+ const results = await backend.queryTraces({});
177
+ assert.strictEqual(results[0].kind, 'INTERNAL');
178
+ assert.strictEqual(results[1].kind, 'SERVER');
179
+ assert.strictEqual(results[2].kind, 'CLIENT');
180
+ });
181
+ it('should convert status code number to string', async () => {
182
+ const today = getTestDate();
183
+ const mockSpans = [
184
+ { traceId: 'trace1', spanId: 'span1', name: 'op1', startTime: [1700000000, 0], status: { code: 0 } },
185
+ { traceId: 'trace2', spanId: 'span2', name: 'op2', startTime: [1700000000, 0], status: { code: 1 } },
186
+ { traceId: 'trace3', spanId: 'span3', name: 'op3', startTime: [1700000000, 0], status: { code: 2, message: 'Test error' } },
187
+ ];
188
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
189
+ const results = await backend.queryTraces({});
190
+ assert.strictEqual(results[0].statusCode, 'UNSET');
191
+ assert.strictEqual(results[0].status?.code, 0);
192
+ assert.strictEqual(results[1].statusCode, 'OK');
193
+ assert.strictEqual(results[1].status?.code, 1);
194
+ assert.strictEqual(results[2].statusCode, 'ERROR');
195
+ assert.strictEqual(results[2].status?.code, 2);
196
+ assert.strictEqual(results[2].status?.message, 'Test error');
197
+ });
198
+ it('should handle spans without status', async () => {
199
+ const today = getTestDate();
200
+ const mockSpans = [
201
+ { traceId: 'trace1', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
202
+ ];
203
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
204
+ const results = await backend.queryTraces({});
205
+ assert.strictEqual(results[0].statusCode, undefined);
206
+ assert.strictEqual(results[0].status, undefined);
207
+ });
208
+ it('should extract instrumentationScope from spans', async () => {
209
+ const today = getTestDate();
210
+ const mockSpans = [
211
+ {
212
+ traceId: 'trace1',
213
+ spanId: 'span1',
214
+ name: 'http-request',
215
+ startTime: [1700000000, 0],
216
+ instrumentationScope: {
217
+ name: '@opentelemetry/instrumentation-http',
218
+ version: '0.48.0',
219
+ schemaUrl: 'https://opentelemetry.io/schemas/1.21.0',
220
+ },
221
+ },
222
+ {
223
+ traceId: 'trace2',
224
+ spanId: 'span2',
225
+ name: 'custom-span',
226
+ startTime: [1700000000, 0],
227
+ instrumentationScope: {
228
+ name: 'custom-hooks',
229
+ },
230
+ },
231
+ {
232
+ traceId: 'trace3',
233
+ spanId: 'span3',
234
+ name: 'no-scope',
235
+ startTime: [1700000000, 0],
236
+ },
237
+ ];
238
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
239
+ const results = await backend.queryTraces({});
240
+ assert.strictEqual(results.length, 3);
241
+ // First span: full scope
242
+ assert.strictEqual(results[0].instrumentationScope?.name, '@opentelemetry/instrumentation-http');
243
+ assert.strictEqual(results[0].instrumentationScope?.version, '0.48.0');
244
+ assert.strictEqual(results[0].instrumentationScope?.schemaUrl, 'https://opentelemetry.io/schemas/1.21.0');
245
+ // Second span: name only
246
+ assert.strictEqual(results[1].instrumentationScope?.name, 'custom-hooks');
247
+ assert.strictEqual(results[1].instrumentationScope?.version, undefined);
248
+ // Third span: no scope
249
+ assert.strictEqual(results[2].instrumentationScope, undefined);
250
+ });
251
+ it('should extract span links from spans', async () => {
252
+ const today = getTestDate();
253
+ const mockSpans = [
254
+ {
255
+ traceId: 'trace1',
256
+ spanId: 'span1',
257
+ name: 'batch-processor',
258
+ startTime: [1700000000, 0],
259
+ links: [
260
+ {
261
+ context: { traceId: 'trace-upstream-1', spanId: 'span-upstream-1' },
262
+ attributes: { 'link.type': 'producer' },
263
+ },
264
+ {
265
+ context: { traceId: 'trace-upstream-2', spanId: 'span-upstream-2' },
266
+ },
267
+ ],
268
+ },
269
+ {
270
+ traceId: 'trace2',
271
+ spanId: 'span2',
272
+ name: 'single-link',
273
+ startTime: [1700000000, 0],
274
+ links: [
275
+ {
276
+ context: { traceId: 'trace-parent', spanId: 'span-parent' },
277
+ attributes: { 'link.reason': 'causal' },
278
+ },
279
+ ],
280
+ },
281
+ {
282
+ traceId: 'trace3',
283
+ spanId: 'span3',
284
+ name: 'no-links',
285
+ startTime: [1700000000, 0],
286
+ },
287
+ ];
288
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
289
+ const results = await backend.queryTraces({});
290
+ assert.strictEqual(results.length, 3);
291
+ // First span: multiple links
292
+ assert.strictEqual(results[0].links?.length, 2);
293
+ assert.strictEqual(results[0].links?.[0].traceId, 'trace-upstream-1');
294
+ assert.strictEqual(results[0].links?.[0].spanId, 'span-upstream-1');
295
+ assert.strictEqual(results[0].links?.[0].attributes?.['link.type'], 'producer');
296
+ assert.strictEqual(results[0].links?.[1].traceId, 'trace-upstream-2');
297
+ assert.strictEqual(results[0].links?.[1].spanId, 'span-upstream-2');
298
+ assert.strictEqual(results[0].links?.[1].attributes, undefined);
299
+ // Second span: single link with attributes
300
+ assert.strictEqual(results[1].links?.length, 1);
301
+ assert.strictEqual(results[1].links?.[0].traceId, 'trace-parent');
302
+ assert.strictEqual(results[1].links?.[0].attributes?.['link.reason'], 'causal');
303
+ // Third span: no links
304
+ assert.strictEqual(results[2].links, undefined);
305
+ });
306
+ it('should filter out invalid span links with missing context', async () => {
307
+ const today = getTestDate();
308
+ const mockSpans = [
309
+ {
310
+ traceId: 'trace1',
311
+ spanId: 'span1',
312
+ name: 'mixed-links',
313
+ startTime: [1700000000, 0],
314
+ links: [
315
+ {
316
+ context: { traceId: 'valid-trace', spanId: 'valid-span' },
317
+ },
318
+ {
319
+ context: { traceId: 'missing-span-id' },
320
+ },
321
+ {
322
+ context: { spanId: 'missing-trace-id' },
323
+ },
324
+ {
325
+ // No context at all
326
+ attributes: { 'orphan': true },
327
+ },
328
+ ],
329
+ },
330
+ ];
331
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
332
+ const results = await backend.queryTraces({});
333
+ assert.strictEqual(results.length, 1);
334
+ // Only the valid link should be included
335
+ assert.strictEqual(results[0].links?.length, 1);
336
+ assert.strictEqual(results[0].links?.[0].traceId, 'valid-trace');
337
+ assert.strictEqual(results[0].links?.[0].spanId, 'valid-span');
338
+ });
339
+ it('should set links to undefined when all links are invalid', async () => {
340
+ const today = getTestDate();
341
+ const mockSpans = [
342
+ {
343
+ traceId: 'trace1',
344
+ spanId: 'span1',
345
+ name: 'all-invalid-links',
346
+ startTime: [1700000000, 0],
347
+ links: [
348
+ { context: { traceId: 'missing-span' } },
349
+ { context: { spanId: 'missing-trace' } },
350
+ ],
351
+ },
352
+ ];
353
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
354
+ const results = await backend.queryTraces({});
355
+ assert.strictEqual(results.length, 1);
356
+ assert.strictEqual(results[0].links, undefined);
357
+ });
358
+ it('should return empty array when no files found', async () => {
359
+ // No files created - tempDir is empty
360
+ const results = await backend.queryTraces({});
361
+ assert.strictEqual(results.length, 0);
362
+ });
363
+ it('should filter spans by attributeFilter with string value', async () => {
364
+ const today = getTestDate();
365
+ const mockSpans = [
366
+ {
367
+ traceId: 'trace1',
368
+ spanId: 'span1',
369
+ name: 'hook:session-start',
370
+ startTime: [1700000000, 0],
371
+ attributes: { 'hook.name': 'session-start', 'hook.type': 'session' },
372
+ },
373
+ {
374
+ traceId: 'trace2',
375
+ spanId: 'span2',
376
+ name: 'hook:mcp-pre-tool',
377
+ startTime: [1700000000, 0],
378
+ attributes: { 'hook.name': 'mcp-pre-tool', 'mcp.server': 'signoz' },
379
+ },
380
+ {
381
+ traceId: 'trace3',
382
+ spanId: 'span3',
383
+ name: 'hook:post-tool',
384
+ startTime: [1700000000, 0],
385
+ attributes: { 'hook.name': 'post-tool', 'mcp.server': 'webresearch' },
386
+ },
387
+ ];
388
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
389
+ const results = await backend.queryTraces({
390
+ attributeFilter: { 'hook.name': 'session-start' },
391
+ });
392
+ assert.strictEqual(results.length, 1);
393
+ assert.strictEqual(results[0].traceId, 'trace1');
394
+ });
395
+ it('should filter spans by attributeFilter with multiple attributes', async () => {
396
+ const today = getTestDate();
397
+ const mockSpans = [
398
+ {
399
+ traceId: 'trace1',
400
+ spanId: 'span1',
401
+ name: 'mcp-call',
402
+ startTime: [1700000000, 0],
403
+ attributes: { 'mcp.server': 'signoz', 'mcp.success': true },
404
+ },
405
+ {
406
+ traceId: 'trace2',
407
+ spanId: 'span2',
408
+ name: 'mcp-call',
409
+ startTime: [1700000000, 0],
410
+ attributes: { 'mcp.server': 'signoz', 'mcp.success': false },
411
+ },
412
+ {
413
+ traceId: 'trace3',
414
+ spanId: 'span3',
415
+ name: 'mcp-call',
416
+ startTime: [1700000000, 0],
417
+ attributes: { 'mcp.server': 'webresearch', 'mcp.success': true },
418
+ },
419
+ ];
420
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
421
+ const results = await backend.queryTraces({
422
+ attributeFilter: { 'mcp.server': 'signoz', 'mcp.success': true },
423
+ });
424
+ assert.strictEqual(results.length, 1);
425
+ assert.strictEqual(results[0].traceId, 'trace1');
426
+ });
427
+ it('should filter spans by attributeFilter with number value', async () => {
428
+ const today = getTestDate();
429
+ const mockSpans = [
430
+ {
431
+ traceId: 'trace1',
432
+ spanId: 'span1',
433
+ name: 'http-request',
434
+ startTime: [1700000000, 0],
435
+ attributes: { 'http.status_code': 200 },
436
+ },
437
+ {
438
+ traceId: 'trace2',
439
+ spanId: 'span2',
440
+ name: 'http-request',
441
+ startTime: [1700000000, 0],
442
+ attributes: { 'http.status_code': 500 },
443
+ },
444
+ ];
445
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
446
+ const results = await backend.queryTraces({
447
+ attributeFilter: { 'http.status_code': 200 },
448
+ });
449
+ assert.strictEqual(results.length, 1);
450
+ assert.strictEqual(results[0].traceId, 'trace1');
451
+ });
452
+ it('should filter spans by attributeFilter with boolean value', async () => {
453
+ const today = getTestDate();
454
+ const mockSpans = [
455
+ {
456
+ traceId: 'trace1',
457
+ spanId: 'span1',
458
+ name: 'agent-call',
459
+ startTime: [1700000000, 0],
460
+ attributes: { 'agent.is_background': true },
461
+ },
462
+ {
463
+ traceId: 'trace2',
464
+ spanId: 'span2',
465
+ name: 'agent-call',
466
+ startTime: [1700000000, 0],
467
+ attributes: { 'agent.is_background': false },
468
+ },
469
+ ];
470
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
471
+ const results = await backend.queryTraces({
472
+ attributeFilter: { 'agent.is_background': false },
473
+ });
474
+ assert.strictEqual(results.length, 1);
475
+ assert.strictEqual(results[0].traceId, 'trace2');
476
+ });
477
+ it('should return empty array when attributeFilter matches nothing', async () => {
478
+ const today = getTestDate();
479
+ const mockSpans = [
480
+ {
481
+ traceId: 'trace1',
482
+ spanId: 'span1',
483
+ name: 'op1',
484
+ startTime: [1700000000, 0],
485
+ attributes: { 'hook.name': 'session-start' },
486
+ },
487
+ ];
488
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
489
+ const results = await backend.queryTraces({
490
+ attributeFilter: { 'hook.name': 'nonexistent' },
491
+ });
492
+ assert.strictEqual(results.length, 0);
493
+ });
494
+ it('should combine attributeFilter with other filters', async () => {
495
+ const today = getTestDate();
496
+ const mockSpans = [
497
+ {
498
+ traceId: 'trace1',
499
+ spanId: 'span1',
500
+ name: 'hook:mcp-pre-tool',
501
+ startTime: [1700000000, 0],
502
+ endTime: [1700000000, 500000000], // 500ms
503
+ attributes: { 'mcp.server': 'signoz' },
504
+ },
505
+ {
506
+ traceId: 'trace2',
507
+ spanId: 'span2',
508
+ name: 'hook:mcp-pre-tool',
509
+ startTime: [1700000000, 0],
510
+ endTime: [1700000002, 0], // 2000ms
511
+ attributes: { 'mcp.server': 'signoz' },
512
+ },
513
+ {
514
+ traceId: 'trace3',
515
+ spanId: 'span3',
516
+ name: 'hook:mcp-pre-tool',
517
+ startTime: [1700000000, 0],
518
+ endTime: [1700000000, 500000000], // 500ms
519
+ attributes: { 'mcp.server': 'webresearch' },
520
+ },
521
+ ];
522
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
523
+ const results = await backend.queryTraces({
524
+ spanName: 'mcp',
525
+ minDurationMs: 1000,
526
+ attributeFilter: { 'mcp.server': 'signoz' },
527
+ });
528
+ assert.strictEqual(results.length, 1);
529
+ assert.strictEqual(results[0].traceId, 'trace2');
530
+ });
531
+ it('should exclude spans matching excludeSpanName', async () => {
532
+ const today = getTestDate();
533
+ const mockSpans = [
534
+ { traceId: 'trace1', spanId: 'span1', name: 'http-request', startTime: [1700000000, 0] },
535
+ { traceId: 'trace2', spanId: 'span2', name: 'db-query', startTime: [1700000000, 0] },
536
+ { traceId: 'trace3', spanId: 'span3', name: 'http-response', startTime: [1700000000, 0] },
537
+ ];
538
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
539
+ const results = await backend.queryTraces({ excludeSpanName: 'http' });
540
+ assert.strictEqual(results.length, 1);
541
+ assert.strictEqual(results[0].name, 'db-query');
542
+ });
543
+ it('should filter spans by spanNameRegex', async () => {
544
+ const today = getTestDate();
545
+ const mockSpans = [
546
+ { traceId: 'trace1', spanId: 'span1', name: 'hook:session-start', startTime: [1700000000, 0] },
547
+ { traceId: 'trace2', spanId: 'span2', name: 'hook:session-end', startTime: [1700000000, 0] },
548
+ { traceId: 'trace3', spanId: 'span3', name: 'mcp-call', startTime: [1700000000, 0] },
549
+ { traceId: 'trace4', spanId: 'span4', name: 'hook:pre-tool', startTime: [1700000000, 0] },
550
+ ];
551
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
552
+ // Match spans starting with 'hook:session'
553
+ const results = await backend.queryTraces({ spanNameRegex: '^hook:session' });
554
+ assert.strictEqual(results.length, 2);
555
+ assert.ok(results.some(s => s.name === 'hook:session-start'));
556
+ assert.ok(results.some(s => s.name === 'hook:session-end'));
557
+ });
558
+ it('should filter spans by spanNameRegex with complex pattern', async () => {
559
+ const today = getTestDate();
560
+ const mockSpans = [
561
+ { traceId: 'trace1', spanId: 'span1', name: 'api-v1-users-get', startTime: [1700000000, 0] },
562
+ { traceId: 'trace2', spanId: 'span2', name: 'api-v2-users-get', startTime: [1700000000, 0] },
563
+ { traceId: 'trace3', spanId: 'span3', name: 'api-v1-orders-post', startTime: [1700000000, 0] },
564
+ { traceId: 'trace4', spanId: 'span4', name: 'internal-process', startTime: [1700000000, 0] },
565
+ ];
566
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
567
+ // Match spans with api-v[12]-.*-get pattern
568
+ const results = await backend.queryTraces({ spanNameRegex: 'api-v[12]-.*-get' });
569
+ assert.strictEqual(results.length, 2);
570
+ assert.ok(results.some(s => s.name === 'api-v1-users-get'));
571
+ assert.ok(results.some(s => s.name === 'api-v2-users-get'));
572
+ });
573
+ it('should handle invalid spanNameRegex gracefully', async () => {
574
+ const today = getTestDate();
575
+ const mockSpans = [
576
+ { traceId: 'trace1', spanId: 'span1', name: 'test-span', startTime: [1700000000, 0] },
577
+ ];
578
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
579
+ // Invalid regex pattern - should be skipped (all spans returned)
580
+ const results = await backend.queryTraces({ spanNameRegex: '[invalid(' });
581
+ // Invalid regex is skipped, so all spans should be returned
582
+ assert.strictEqual(results.length, 1);
583
+ assert.strictEqual(results[0].name, 'test-span');
584
+ });
585
+ it('should combine spanNameRegex with spanName filter', async () => {
586
+ const today = getTestDate();
587
+ const mockSpans = [
588
+ { traceId: 'trace1', spanId: 'span1', name: 'hook:mcp-pre-tool', startTime: [1700000000, 0] },
589
+ { traceId: 'trace2', spanId: 'span2', name: 'hook:mcp-post-tool', startTime: [1700000000, 0] },
590
+ { traceId: 'trace3', spanId: 'span3', name: 'hook:session-start', startTime: [1700000000, 0] },
591
+ { traceId: 'trace4', spanId: 'span4', name: 'mcp-call', startTime: [1700000000, 0] },
592
+ ];
593
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
594
+ // spanName filters first (substring), then regex narrows down
595
+ const results = await backend.queryTraces({
596
+ spanName: 'hook',
597
+ spanNameRegex: 'mcp',
598
+ });
599
+ assert.strictEqual(results.length, 2);
600
+ assert.ok(results.some(s => s.name === 'hook:mcp-pre-tool'));
601
+ assert.ok(results.some(s => s.name === 'hook:mcp-post-tool'));
602
+ });
603
+ it('should combine spanNameRegex with excludeSpanName', async () => {
604
+ const today = getTestDate();
605
+ const mockSpans = [
606
+ { traceId: 'trace1', spanId: 'span1', name: 'hook:mcp-pre-tool', startTime: [1700000000, 0] },
607
+ { traceId: 'trace2', spanId: 'span2', name: 'hook:mcp-post-tool', startTime: [1700000000, 0] },
608
+ { traceId: 'trace3', spanId: 'span3', name: 'hook:session-start', startTime: [1700000000, 0] },
609
+ ];
610
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
611
+ // Regex matches all hook:mcp-*, exclude post-tool
612
+ const results = await backend.queryTraces({
613
+ spanNameRegex: '^hook:mcp-',
614
+ excludeSpanName: 'post-tool',
615
+ });
616
+ assert.strictEqual(results.length, 1);
617
+ assert.strictEqual(results[0].name, 'hook:mcp-pre-tool');
618
+ });
619
+ it('should filter spans by attributeExists - all must exist', async () => {
620
+ const today = getTestDate();
621
+ const mockSpans = [
622
+ {
623
+ traceId: 'trace1',
624
+ spanId: 'span1',
625
+ name: 'op1',
626
+ startTime: [1700000000, 0],
627
+ attributes: { 'http.method': 'GET', 'http.status_code': 200 },
628
+ },
629
+ {
630
+ traceId: 'trace2',
631
+ spanId: 'span2',
632
+ name: 'op2',
633
+ startTime: [1700000000, 0],
634
+ attributes: { 'http.method': 'POST' }, // missing http.status_code
635
+ },
636
+ {
637
+ traceId: 'trace3',
638
+ spanId: 'span3',
639
+ name: 'op3',
640
+ startTime: [1700000000, 0],
641
+ attributes: { 'db.system': 'postgres' }, // missing both
642
+ },
643
+ ];
644
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
645
+ const results = await backend.queryTraces({
646
+ attributeExists: ['http.method', 'http.status_code'],
647
+ });
648
+ assert.strictEqual(results.length, 1);
649
+ assert.strictEqual(results[0].traceId, 'trace1');
650
+ });
651
+ it('should filter spans by attributeNotExists - exclude if any exist', async () => {
652
+ const today = getTestDate();
653
+ const mockSpans = [
654
+ {
655
+ traceId: 'trace1',
656
+ spanId: 'span1',
657
+ name: 'op1',
658
+ startTime: [1700000000, 0],
659
+ attributes: { 'http.method': 'GET', 'error.message': 'timeout' },
660
+ },
661
+ {
662
+ traceId: 'trace2',
663
+ spanId: 'span2',
664
+ name: 'op2',
665
+ startTime: [1700000000, 0],
666
+ attributes: { 'http.method': 'POST' }, // no error attributes
667
+ },
668
+ {
669
+ traceId: 'trace3',
670
+ spanId: 'span3',
671
+ name: 'op3',
672
+ startTime: [1700000000, 0],
673
+ attributes: { 'http.method': 'GET', 'error.type': 'network' },
674
+ },
675
+ ];
676
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
677
+ const results = await backend.queryTraces({
678
+ attributeNotExists: ['error.message', 'error.type'],
679
+ });
680
+ assert.strictEqual(results.length, 1);
681
+ assert.strictEqual(results[0].traceId, 'trace2');
682
+ });
683
+ it('should combine spanName with excludeSpanName', async () => {
684
+ const today = getTestDate();
685
+ const mockSpans = [
686
+ { traceId: 'trace1', spanId: 'span1', name: 'http-request-external', startTime: [1700000000, 0] },
687
+ { traceId: 'trace2', spanId: 'span2', name: 'http-request-internal', startTime: [1700000000, 0] },
688
+ { traceId: 'trace3', spanId: 'span3', name: 'db-query', startTime: [1700000000, 0] },
689
+ ];
690
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
691
+ const results = await backend.queryTraces({
692
+ spanName: 'http',
693
+ excludeSpanName: 'internal',
694
+ });
695
+ assert.strictEqual(results.length, 1);
696
+ assert.strictEqual(results[0].name, 'http-request-external');
697
+ });
698
+ it('should combine attributeExists with attributeFilter', async () => {
699
+ const today = getTestDate();
700
+ const mockSpans = [
701
+ {
702
+ traceId: 'trace1',
703
+ spanId: 'span1',
704
+ name: 'op1',
705
+ startTime: [1700000000, 0],
706
+ attributes: { 'http.method': 'GET', 'http.status_code': 200 },
707
+ },
708
+ {
709
+ traceId: 'trace2',
710
+ spanId: 'span2',
711
+ name: 'op2',
712
+ startTime: [1700000000, 0],
713
+ attributes: { 'http.method': 'POST', 'http.status_code': 500 },
714
+ },
715
+ {
716
+ traceId: 'trace3',
717
+ spanId: 'span3',
718
+ name: 'op3',
719
+ startTime: [1700000000, 0],
720
+ attributes: { 'http.method': 'GET' }, // missing status_code
721
+ },
722
+ ];
723
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
724
+ const results = await backend.queryTraces({
725
+ attributeFilter: { 'http.method': 'GET' },
726
+ attributeExists: ['http.status_code'],
727
+ });
728
+ assert.strictEqual(results.length, 1);
729
+ assert.strictEqual(results[0].traceId, 'trace1');
730
+ });
731
+ it('should filter spans by numericFilter with gt operator', async () => {
732
+ const today = getTestDate();
733
+ const mockSpans = [
734
+ {
735
+ traceId: 'trace1',
736
+ spanId: 'span1',
737
+ name: 'http-request',
738
+ startTime: [1700000000, 0],
739
+ attributes: { 'http.status_code': 200 },
740
+ },
741
+ {
742
+ traceId: 'trace2',
743
+ spanId: 'span2',
744
+ name: 'http-request',
745
+ startTime: [1700000000, 0],
746
+ attributes: { 'http.status_code': 500 },
747
+ },
748
+ {
749
+ traceId: 'trace3',
750
+ spanId: 'span3',
751
+ name: 'http-request',
752
+ startTime: [1700000000, 0],
753
+ attributes: { 'http.status_code': 300 },
754
+ },
755
+ ];
756
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
757
+ const results = await backend.queryTraces({
758
+ numericFilter: [{ attribute: 'http.status_code', operator: 'gt', value: 299 }],
759
+ });
760
+ assert.strictEqual(results.length, 2);
761
+ assert.ok(results.some(s => s.traceId === 'trace2'));
762
+ assert.ok(results.some(s => s.traceId === 'trace3'));
763
+ });
764
+ it('should filter spans by numericFilter with gte operator', async () => {
765
+ const today = getTestDate();
766
+ const mockSpans = [
767
+ {
768
+ traceId: 'trace1',
769
+ spanId: 'span1',
770
+ name: 'http-request',
771
+ startTime: [1700000000, 0],
772
+ attributes: { 'http.status_code': 200 },
773
+ },
774
+ {
775
+ traceId: 'trace2',
776
+ spanId: 'span2',
777
+ name: 'http-request',
778
+ startTime: [1700000000, 0],
779
+ attributes: { 'http.status_code': 300 },
780
+ },
781
+ ];
782
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
783
+ const results = await backend.queryTraces({
784
+ numericFilter: [{ attribute: 'http.status_code', operator: 'gte', value: 300 }],
785
+ });
786
+ assert.strictEqual(results.length, 1);
787
+ assert.strictEqual(results[0].traceId, 'trace2');
788
+ });
789
+ it('should filter spans by numericFilter with lt operator', async () => {
790
+ const today = getTestDate();
791
+ const mockSpans = [
792
+ {
793
+ traceId: 'trace1',
794
+ spanId: 'span1',
795
+ name: 'http-request',
796
+ startTime: [1700000000, 0],
797
+ attributes: { 'http.status_code': 200 },
798
+ },
799
+ {
800
+ traceId: 'trace2',
801
+ spanId: 'span2',
802
+ name: 'http-request',
803
+ startTime: [1700000000, 0],
804
+ attributes: { 'http.status_code': 500 },
805
+ },
806
+ ];
807
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
808
+ const results = await backend.queryTraces({
809
+ numericFilter: [{ attribute: 'http.status_code', operator: 'lt', value: 300 }],
810
+ });
811
+ assert.strictEqual(results.length, 1);
812
+ assert.strictEqual(results[0].traceId, 'trace1');
813
+ });
814
+ it('should filter spans by numericFilter with lte operator', async () => {
815
+ const today = getTestDate();
816
+ const mockSpans = [
817
+ {
818
+ traceId: 'trace1',
819
+ spanId: 'span1',
820
+ name: 'http-request',
821
+ startTime: [1700000000, 0],
822
+ attributes: { 'http.status_code': 200 },
823
+ },
824
+ {
825
+ traceId: 'trace2',
826
+ spanId: 'span2',
827
+ name: 'http-request',
828
+ startTime: [1700000000, 0],
829
+ attributes: { 'http.status_code': 300 },
830
+ },
831
+ ];
832
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
833
+ const results = await backend.queryTraces({
834
+ numericFilter: [{ attribute: 'http.status_code', operator: 'lte', value: 200 }],
835
+ });
836
+ assert.strictEqual(results.length, 1);
837
+ assert.strictEqual(results[0].traceId, 'trace1');
838
+ });
839
+ it('should filter spans by numericFilter with eq operator', async () => {
840
+ const today = getTestDate();
841
+ const mockSpans = [
842
+ {
843
+ traceId: 'trace1',
844
+ spanId: 'span1',
845
+ name: 'http-request',
846
+ startTime: [1700000000, 0],
847
+ attributes: { 'http.status_code': 200 },
848
+ },
849
+ {
850
+ traceId: 'trace2',
851
+ spanId: 'span2',
852
+ name: 'http-request',
853
+ startTime: [1700000000, 0],
854
+ attributes: { 'http.status_code': 500 },
855
+ },
856
+ ];
857
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
858
+ const results = await backend.queryTraces({
859
+ numericFilter: [{ attribute: 'http.status_code', operator: 'eq', value: 200 }],
860
+ });
861
+ assert.strictEqual(results.length, 1);
862
+ assert.strictEqual(results[0].traceId, 'trace1');
863
+ });
864
+ it('should filter spans by multiple numericFilter conditions (AND logic)', async () => {
865
+ const today = getTestDate();
866
+ const mockSpans = [
867
+ {
868
+ traceId: 'trace1',
869
+ spanId: 'span1',
870
+ name: 'http-request',
871
+ startTime: [1700000000, 0],
872
+ attributes: { 'http.status_code': 200, 'http.response_size': 1000 },
873
+ },
874
+ {
875
+ traceId: 'trace2',
876
+ spanId: 'span2',
877
+ name: 'http-request',
878
+ startTime: [1700000000, 0],
879
+ attributes: { 'http.status_code': 200, 'http.response_size': 5000 },
880
+ },
881
+ {
882
+ traceId: 'trace3',
883
+ spanId: 'span3',
884
+ name: 'http-request',
885
+ startTime: [1700000000, 0],
886
+ attributes: { 'http.status_code': 500, 'http.response_size': 100 },
887
+ },
888
+ ];
889
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
890
+ const results = await backend.queryTraces({
891
+ numericFilter: [
892
+ { attribute: 'http.status_code', operator: 'lt', value: 300 },
893
+ { attribute: 'http.response_size', operator: 'gt', value: 2000 },
894
+ ],
895
+ });
896
+ assert.strictEqual(results.length, 1);
897
+ assert.strictEqual(results[0].traceId, 'trace2');
898
+ });
899
+ it('should skip spans when numericFilter attribute is missing', async () => {
900
+ const today = getTestDate();
901
+ const mockSpans = [
902
+ {
903
+ traceId: 'trace1',
904
+ spanId: 'span1',
905
+ name: 'http-request',
906
+ startTime: [1700000000, 0],
907
+ attributes: { 'http.status_code': 200 },
908
+ },
909
+ {
910
+ traceId: 'trace2',
911
+ spanId: 'span2',
912
+ name: 'http-request',
913
+ startTime: [1700000000, 0],
914
+ attributes: { 'other.attr': 'value' }, // missing http.status_code
915
+ },
916
+ ];
917
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
918
+ const results = await backend.queryTraces({
919
+ numericFilter: [{ attribute: 'http.status_code', operator: 'gte', value: 100 }],
920
+ });
921
+ assert.strictEqual(results.length, 1);
922
+ assert.strictEqual(results[0].traceId, 'trace1');
923
+ });
924
+ it('should skip spans when numericFilter attribute is not a number', async () => {
925
+ const today = getTestDate();
926
+ const mockSpans = [
927
+ {
928
+ traceId: 'trace1',
929
+ spanId: 'span1',
930
+ name: 'http-request',
931
+ startTime: [1700000000, 0],
932
+ attributes: { 'http.status_code': 200 },
933
+ },
934
+ {
935
+ traceId: 'trace2',
936
+ spanId: 'span2',
937
+ name: 'http-request',
938
+ startTime: [1700000000, 0],
939
+ attributes: { 'http.status_code': '200' }, // string, not number
940
+ },
941
+ ];
942
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
943
+ const results = await backend.queryTraces({
944
+ numericFilter: [{ attribute: 'http.status_code', operator: 'eq', value: 200 }],
945
+ });
946
+ assert.strictEqual(results.length, 1);
947
+ assert.strictEqual(results[0].traceId, 'trace1');
948
+ });
949
+ it('should combine numericFilter with other filters', async () => {
950
+ const today = getTestDate();
951
+ const mockSpans = [
952
+ {
953
+ traceId: 'trace1',
954
+ spanId: 'span1',
955
+ name: 'http-request',
956
+ startTime: [1700000000, 0],
957
+ attributes: { 'http.status_code': 500, 'http.method': 'GET' },
958
+ },
959
+ {
960
+ traceId: 'trace2',
961
+ spanId: 'span2',
962
+ name: 'http-request',
963
+ startTime: [1700000000, 0],
964
+ attributes: { 'http.status_code': 500, 'http.method': 'POST' },
965
+ },
966
+ {
967
+ traceId: 'trace3',
968
+ spanId: 'span3',
969
+ name: 'http-request',
970
+ startTime: [1700000000, 0],
971
+ attributes: { 'http.status_code': 200, 'http.method': 'GET' },
972
+ },
973
+ ];
974
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
975
+ const results = await backend.queryTraces({
976
+ attributeFilter: { 'http.method': 'GET' },
977
+ numericFilter: [{ attribute: 'http.status_code', operator: 'gte', value: 400 }],
978
+ });
979
+ assert.strictEqual(results.length, 1);
980
+ assert.strictEqual(results[0].traceId, 'trace1');
981
+ });
982
+ it('should complete queries with timing (timing is logged for slow queries)', async () => {
983
+ // This test verifies that query timing is active and doesn't break normal queries
984
+ // Timing warnings are logged for queries > 500ms
985
+ const today = getTestDate();
986
+ const mockSpans = [
987
+ { traceId: 'trace1', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
988
+ { traceId: 'trace2', spanId: 'span2', name: 'op2', startTime: [1700000001, 0] },
989
+ ];
990
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
991
+ // Query should complete successfully with timing active
992
+ const results = await backend.queryTraces({});
993
+ assert.strictEqual(results.length, 2);
994
+ });
995
+ });
996
+ describe('queryLLMEvents', () => {
997
+ it('should read and normalize LLM events from JSONL files', async () => {
998
+ const today = getTestDate();
999
+ const mockEvents = [
1000
+ {
1001
+ timestamp: '2026-01-28T10:00:00.000Z',
1002
+ name: 'llm.completion',
1003
+ attributes: {
1004
+ 'gen_ai.request.model': 'claude-3-opus',
1005
+ 'gen_ai.system': 'anthropic',
1006
+ 'gen_ai.usage.input_tokens': 100,
1007
+ 'gen_ai.usage.output_tokens': 50,
1008
+ 'duration_ms': 1500,
1009
+ 'success': true,
1010
+ },
1011
+ },
1012
+ ];
1013
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1014
+ const results = await backend.queryLLMEvents({});
1015
+ assert.strictEqual(results.length, 1);
1016
+ assert.strictEqual(results[0].name, 'llm.completion');
1017
+ assert.strictEqual(results[0].attributes['gen_ai.request.model'], 'claude-3-opus');
1018
+ assert.strictEqual(results[0].attributes['gen_ai.system'], 'anthropic');
1019
+ assert.strictEqual(results[0].attributes['gen_ai.usage.input_tokens'], 100);
1020
+ });
1021
+ it('should filter events by eventName substring', async () => {
1022
+ const today = getTestDate();
1023
+ const mockEvents = [
1024
+ { timestamp: '2026-01-28T10:00:00Z', name: 'llm.completion', attributes: {} },
1025
+ { timestamp: '2026-01-28T10:01:00Z', name: 'llm.embedding', attributes: {} },
1026
+ { timestamp: '2026-01-28T10:02:00Z', name: 'tool.execution', attributes: {} },
1027
+ ];
1028
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1029
+ const results = await backend.queryLLMEvents({ eventName: 'llm' });
1030
+ assert.strictEqual(results.length, 2);
1031
+ assert.ok(results.every(e => e.name.includes('llm')));
1032
+ });
1033
+ it('should filter events by model', async () => {
1034
+ const today = getTestDate();
1035
+ const mockEvents = [
1036
+ {
1037
+ timestamp: '2026-01-28T10:00:00Z',
1038
+ name: 'llm.completion',
1039
+ attributes: { 'gen_ai.request.model': 'claude-3-opus' },
1040
+ },
1041
+ {
1042
+ timestamp: '2026-01-28T10:01:00Z',
1043
+ name: 'llm.completion',
1044
+ attributes: { 'gen_ai.request.model': 'gpt-4' },
1045
+ },
1046
+ {
1047
+ timestamp: '2026-01-28T10:02:00Z',
1048
+ name: 'llm.completion',
1049
+ attributes: { model: 'claude-3-opus' }, // alternate attribute name
1050
+ },
1051
+ ];
1052
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1053
+ const results = await backend.queryLLMEvents({ model: 'claude-3-opus' });
1054
+ assert.strictEqual(results.length, 2);
1055
+ });
1056
+ it('should filter events by provider', async () => {
1057
+ const today = getTestDate();
1058
+ const mockEvents = [
1059
+ {
1060
+ timestamp: '2026-01-28T10:00:00Z',
1061
+ name: 'llm.completion',
1062
+ attributes: { 'gen_ai.system': 'anthropic' },
1063
+ },
1064
+ {
1065
+ timestamp: '2026-01-28T10:01:00Z',
1066
+ name: 'llm.completion',
1067
+ attributes: { 'gen_ai.system': 'openai' },
1068
+ },
1069
+ {
1070
+ timestamp: '2026-01-28T10:02:00Z',
1071
+ name: 'llm.completion',
1072
+ attributes: { provider: 'anthropic' }, // alternate attribute name
1073
+ },
1074
+ ];
1075
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1076
+ const results = await backend.queryLLMEvents({ provider: 'anthropic' });
1077
+ assert.strictEqual(results.length, 2);
1078
+ });
1079
+ it('should filter events by search text in attributes', async () => {
1080
+ const today = getTestDate();
1081
+ const mockEvents = [
1082
+ {
1083
+ timestamp: '2026-01-28T10:00:00Z',
1084
+ name: 'llm.completion',
1085
+ attributes: { prompt: 'Write a function to calculate fibonacci' },
1086
+ },
1087
+ {
1088
+ timestamp: '2026-01-28T10:01:00Z',
1089
+ name: 'llm.completion',
1090
+ attributes: { prompt: 'Explain quantum computing' },
1091
+ },
1092
+ ];
1093
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1094
+ const results = await backend.queryLLMEvents({ search: 'fibonacci' });
1095
+ assert.strictEqual(results.length, 1);
1096
+ assert.strictEqual(results[0].attributes.prompt, 'Write a function to calculate fibonacci');
1097
+ });
1098
+ it('should filter events by search text in event name', async () => {
1099
+ const today = getTestDate();
1100
+ const mockEvents = [
1101
+ { timestamp: '2026-01-28T10:00:00Z', name: 'llm.completion.streaming', attributes: {} },
1102
+ { timestamp: '2026-01-28T10:01:00Z', name: 'llm.completion', attributes: {} },
1103
+ ];
1104
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1105
+ const results = await backend.queryLLMEvents({ search: 'streaming' });
1106
+ assert.strictEqual(results.length, 1);
1107
+ assert.strictEqual(results[0].name, 'llm.completion.streaming');
1108
+ });
1109
+ it('should apply limit and offset to LLM event results', async () => {
1110
+ const today = getTestDate();
1111
+ const mockEvents = Array.from({ length: 100 }, (_, i) => ({
1112
+ timestamp: new Date(Date.now() + i * 1000).toISOString(),
1113
+ name: `event-${i}`,
1114
+ attributes: { index: i },
1115
+ }));
1116
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1117
+ const results = await backend.queryLLMEvents({ limit: 20, offset: 50 });
1118
+ assert.strictEqual(results.length, 20);
1119
+ assert.strictEqual(results[0].name, 'event-50');
1120
+ });
1121
+ it('should filter events by date range', async () => {
1122
+ // Create files for multiple dates
1123
+ await writeJsonlFileAsync(path.join(tempDir, 'llm-events-2026-01-26.jsonl'), [
1124
+ { timestamp: '2026-01-26T10:00:00Z', name: 'event-26', attributes: {} },
1125
+ ]);
1126
+ await writeJsonlFileAsync(path.join(tempDir, 'llm-events-2026-01-27.jsonl'), [
1127
+ { timestamp: '2026-01-27T10:00:00Z', name: 'event-27', attributes: {} },
1128
+ ]);
1129
+ await writeJsonlFileAsync(path.join(tempDir, 'llm-events-2026-01-28.jsonl'), [
1130
+ { timestamp: '2026-01-28T10:00:00Z', name: 'event-28', attributes: {} },
1131
+ ]);
1132
+ const results = await backend.queryLLMEvents({
1133
+ startDate: '2026-01-27',
1134
+ endDate: '2026-01-27',
1135
+ });
1136
+ assert.strictEqual(results.length, 1);
1137
+ assert.strictEqual(results[0].name, 'event-27');
1138
+ });
1139
+ it('should skip events with missing required fields', async () => {
1140
+ const today = getTestDate();
1141
+ const mockEvents = [
1142
+ { timestamp: '2026-01-28T10:00:00Z', name: 'valid-event', attributes: {} },
1143
+ { timestamp: '2026-01-28T10:01:00Z', attributes: {} }, // missing name
1144
+ { name: 'no-timestamp', attributes: {} }, // missing timestamp
1145
+ ];
1146
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1147
+ const results = await backend.queryLLMEvents({});
1148
+ assert.strictEqual(results.length, 1);
1149
+ assert.strictEqual(results[0].name, 'valid-event');
1150
+ });
1151
+ it('should return empty array when no LLM event files found', async () => {
1152
+ const results = await backend.queryLLMEvents({});
1153
+ assert.strictEqual(results.length, 0);
1154
+ });
1155
+ it('should combine multiple filters', async () => {
1156
+ const today = getTestDate();
1157
+ const mockEvents = [
1158
+ {
1159
+ timestamp: '2026-01-28T10:00:00Z',
1160
+ name: 'llm.completion',
1161
+ attributes: { 'gen_ai.request.model': 'claude-3-opus', 'gen_ai.system': 'anthropic' },
1162
+ },
1163
+ {
1164
+ timestamp: '2026-01-28T10:01:00Z',
1165
+ name: 'llm.completion',
1166
+ attributes: { 'gen_ai.request.model': 'gpt-4', 'gen_ai.system': 'openai' },
1167
+ },
1168
+ {
1169
+ timestamp: '2026-01-28T10:02:00Z',
1170
+ name: 'llm.embedding',
1171
+ attributes: { 'gen_ai.request.model': 'claude-3-opus', 'gen_ai.system': 'anthropic' },
1172
+ },
1173
+ ];
1174
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1175
+ const results = await backend.queryLLMEvents({
1176
+ eventName: 'completion',
1177
+ model: 'claude-3-opus',
1178
+ provider: 'anthropic',
1179
+ });
1180
+ assert.strictEqual(results.length, 1);
1181
+ assert.strictEqual(results[0].name, 'llm.completion');
1182
+ });
1183
+ it('should use OTel GenAI provider fallback: gen_ai.provider.name -> gen_ai.system -> provider', async () => {
1184
+ const today = getTestDate();
1185
+ const mockEvents = [
1186
+ {
1187
+ timestamp: '2026-01-28T10:00:00Z',
1188
+ name: 'llm.completion',
1189
+ attributes: { 'gen_ai.provider.name': 'anthropic-new' }, // should match
1190
+ },
1191
+ {
1192
+ timestamp: '2026-01-28T10:01:00Z',
1193
+ name: 'llm.completion',
1194
+ attributes: { 'gen_ai.system': 'anthropic-new', 'provider': 'legacy' }, // should match via gen_ai.system
1195
+ },
1196
+ {
1197
+ timestamp: '2026-01-28T10:02:00Z',
1198
+ name: 'llm.completion',
1199
+ attributes: { 'provider': 'anthropic-new' }, // should match via provider fallback
1200
+ },
1201
+ {
1202
+ timestamp: '2026-01-28T10:03:00Z',
1203
+ name: 'llm.completion',
1204
+ attributes: { 'gen_ai.provider.name': 'other-provider' }, // should NOT match
1205
+ },
1206
+ ];
1207
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1208
+ const results = await backend.queryLLMEvents({ provider: 'anthropic-new' });
1209
+ assert.strictEqual(results.length, 3);
1210
+ });
1211
+ it('should filter OpenAI events by provider', async () => {
1212
+ const today = getTestDate();
1213
+ const mockEvents = [
1214
+ {
1215
+ timestamp: '2026-01-28T10:00:00Z',
1216
+ name: 'llm.completion',
1217
+ attributes: {
1218
+ 'gen_ai.provider.name': 'openai',
1219
+ 'gen_ai.request.model': 'gpt-4o',
1220
+ 'gen_ai.usage.input_tokens': 500,
1221
+ },
1222
+ },
1223
+ {
1224
+ timestamp: '2026-01-28T10:01:00Z',
1225
+ name: 'llm.completion',
1226
+ attributes: {
1227
+ 'gen_ai.provider.name': 'anthropic',
1228
+ 'gen_ai.request.model': 'claude-3-opus',
1229
+ },
1230
+ },
1231
+ ];
1232
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1233
+ const results = await backend.queryLLMEvents({ provider: 'openai' });
1234
+ assert.strictEqual(results.length, 1);
1235
+ assert.strictEqual(results[0].attributes?.['gen_ai.request.model'], 'gpt-4o');
1236
+ });
1237
+ it('should filter Google Gemini events by provider', async () => {
1238
+ const today = getTestDate();
1239
+ const mockEvents = [
1240
+ {
1241
+ timestamp: '2026-01-28T10:00:00Z',
1242
+ name: 'llm.completion',
1243
+ attributes: {
1244
+ 'gen_ai.provider.name': 'gcp.gemini',
1245
+ 'gen_ai.request.model': 'gemini-1.5-pro',
1246
+ },
1247
+ },
1248
+ {
1249
+ timestamp: '2026-01-28T10:01:00Z',
1250
+ name: 'llm.completion',
1251
+ attributes: {
1252
+ 'gen_ai.provider.name': 'gcp.vertex_ai',
1253
+ 'gen_ai.request.model': 'gemini-pro',
1254
+ },
1255
+ },
1256
+ {
1257
+ timestamp: '2026-01-28T10:02:00Z',
1258
+ name: 'llm.completion',
1259
+ attributes: {
1260
+ 'gen_ai.provider.name': 'openai',
1261
+ 'gen_ai.request.model': 'gpt-4',
1262
+ },
1263
+ },
1264
+ ];
1265
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1266
+ const geminiResults = await backend.queryLLMEvents({ provider: 'gcp.gemini' });
1267
+ assert.strictEqual(geminiResults.length, 1);
1268
+ assert.strictEqual(geminiResults[0].attributes?.['gen_ai.request.model'], 'gemini-1.5-pro');
1269
+ const vertexResults = await backend.queryLLMEvents({ provider: 'gcp.vertex_ai' });
1270
+ assert.strictEqual(vertexResults.length, 1);
1271
+ });
1272
+ it('should filter Mistral AI events by provider', async () => {
1273
+ const today = getTestDate();
1274
+ const mockEvents = [
1275
+ {
1276
+ timestamp: '2026-01-28T10:00:00Z',
1277
+ name: 'llm.completion',
1278
+ attributes: {
1279
+ 'gen_ai.provider.name': 'mistral_ai',
1280
+ 'gen_ai.request.model': 'mistral-large',
1281
+ },
1282
+ },
1283
+ {
1284
+ timestamp: '2026-01-28T10:01:00Z',
1285
+ name: 'llm.completion',
1286
+ attributes: {
1287
+ 'gen_ai.provider.name': 'anthropic',
1288
+ 'gen_ai.request.model': 'claude-3',
1289
+ },
1290
+ },
1291
+ ];
1292
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1293
+ const results = await backend.queryLLMEvents({ provider: 'mistral_ai' });
1294
+ assert.strictEqual(results.length, 1);
1295
+ assert.strictEqual(results[0].attributes?.['gen_ai.request.model'], 'mistral-large');
1296
+ });
1297
+ it('should filter AWS Bedrock events by provider', async () => {
1298
+ const today = getTestDate();
1299
+ const mockEvents = [
1300
+ {
1301
+ timestamp: '2026-01-28T10:00:00Z',
1302
+ name: 'llm.completion',
1303
+ attributes: {
1304
+ 'gen_ai.provider.name': 'aws.bedrock',
1305
+ 'gen_ai.request.model': 'anthropic.claude-3-sonnet-20240229-v1:0',
1306
+ },
1307
+ },
1308
+ {
1309
+ timestamp: '2026-01-28T10:01:00Z',
1310
+ name: 'llm.completion',
1311
+ attributes: {
1312
+ 'gen_ai.provider.name': 'anthropic',
1313
+ 'gen_ai.request.model': 'claude-3-sonnet',
1314
+ },
1315
+ },
1316
+ ];
1317
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1318
+ const results = await backend.queryLLMEvents({ provider: 'aws.bedrock' });
1319
+ assert.strictEqual(results.length, 1);
1320
+ assert.ok(results[0].attributes?.['gen_ai.request.model']?.toString().includes('anthropic.claude'));
1321
+ });
1322
+ it('should filter Cohere events by provider', async () => {
1323
+ const today = getTestDate();
1324
+ const mockEvents = [
1325
+ {
1326
+ timestamp: '2026-01-28T10:00:00Z',
1327
+ name: 'llm.completion',
1328
+ attributes: {
1329
+ 'gen_ai.provider.name': 'cohere',
1330
+ 'gen_ai.request.model': 'command-r-plus',
1331
+ },
1332
+ },
1333
+ {
1334
+ timestamp: '2026-01-28T10:01:00Z',
1335
+ name: 'llm.completion',
1336
+ attributes: {
1337
+ 'gen_ai.provider.name': 'openai',
1338
+ 'gen_ai.request.model': 'gpt-4',
1339
+ },
1340
+ },
1341
+ ];
1342
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1343
+ const results = await backend.queryLLMEvents({ provider: 'cohere' });
1344
+ assert.strictEqual(results.length, 1);
1345
+ assert.strictEqual(results[0].attributes?.['gen_ai.request.model'], 'command-r-plus');
1346
+ });
1347
+ it('should filter Groq events by provider', async () => {
1348
+ const today = getTestDate();
1349
+ const mockEvents = [
1350
+ {
1351
+ timestamp: '2026-01-28T10:00:00Z',
1352
+ name: 'llm.completion',
1353
+ attributes: {
1354
+ 'gen_ai.provider.name': 'groq',
1355
+ 'gen_ai.request.model': 'llama-3.3-70b',
1356
+ },
1357
+ },
1358
+ {
1359
+ timestamp: '2026-01-28T10:01:00Z',
1360
+ name: 'llm.completion',
1361
+ attributes: {
1362
+ 'gen_ai.provider.name': 'together_ai',
1363
+ 'gen_ai.request.model': 'llama-3-70b',
1364
+ },
1365
+ },
1366
+ ];
1367
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1368
+ const groqResults = await backend.queryLLMEvents({ provider: 'groq' });
1369
+ assert.strictEqual(groqResults.length, 1);
1370
+ assert.strictEqual(groqResults[0].attributes?.['gen_ai.request.model'], 'llama-3.3-70b');
1371
+ const togetherResults = await backend.queryLLMEvents({ provider: 'together_ai' });
1372
+ assert.strictEqual(togetherResults.length, 1);
1373
+ });
1374
+ it('should filter Ollama local model events by provider', async () => {
1375
+ const today = getTestDate();
1376
+ const mockEvents = [
1377
+ {
1378
+ timestamp: '2026-01-28T10:00:00Z',
1379
+ name: 'llm.completion',
1380
+ attributes: {
1381
+ 'gen_ai.provider.name': 'ollama',
1382
+ 'gen_ai.request.model': 'llama3:8b',
1383
+ },
1384
+ },
1385
+ {
1386
+ timestamp: '2026-01-28T10:01:00Z',
1387
+ name: 'llm.completion',
1388
+ attributes: {
1389
+ 'gen_ai.provider.name': 'openai',
1390
+ 'gen_ai.request.model': 'gpt-4',
1391
+ },
1392
+ },
1393
+ ];
1394
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1395
+ const results = await backend.queryLLMEvents({ provider: 'ollama' });
1396
+ assert.strictEqual(results.length, 1);
1397
+ assert.strictEqual(results[0].attributes?.['gen_ai.request.model'], 'llama3:8b');
1398
+ });
1399
+ it('should filter custom/internal provider events', async () => {
1400
+ const today = getTestDate();
1401
+ const mockEvents = [
1402
+ {
1403
+ timestamp: '2026-01-28T10:00:00Z',
1404
+ name: 'llm.completion',
1405
+ attributes: {
1406
+ 'gen_ai.provider.name': 'custom-internal-llm',
1407
+ 'gen_ai.request.model': 'internal-model-v2',
1408
+ },
1409
+ },
1410
+ {
1411
+ timestamp: '2026-01-28T10:01:00Z',
1412
+ name: 'llm.completion',
1413
+ attributes: {
1414
+ 'gen_ai.provider.name': 'anthropic',
1415
+ 'gen_ai.request.model': 'claude-3',
1416
+ },
1417
+ },
1418
+ ];
1419
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1420
+ const results = await backend.queryLLMEvents({ provider: 'custom-internal-llm' });
1421
+ assert.strictEqual(results.length, 1);
1422
+ assert.strictEqual(results[0].attributes?.['gen_ai.request.model'], 'internal-model-v2');
1423
+ });
1424
+ it('should combine provider and model filters', async () => {
1425
+ const today = getTestDate();
1426
+ const mockEvents = [
1427
+ {
1428
+ timestamp: '2026-01-28T10:00:00Z',
1429
+ name: 'llm.completion',
1430
+ attributes: {
1431
+ 'gen_ai.provider.name': 'openai',
1432
+ 'gen_ai.request.model': 'gpt-4o',
1433
+ },
1434
+ },
1435
+ {
1436
+ timestamp: '2026-01-28T10:01:00Z',
1437
+ name: 'llm.completion',
1438
+ attributes: {
1439
+ 'gen_ai.provider.name': 'openai',
1440
+ 'gen_ai.request.model': 'gpt-4-turbo',
1441
+ },
1442
+ },
1443
+ {
1444
+ timestamp: '2026-01-28T10:02:00Z',
1445
+ name: 'llm.completion',
1446
+ attributes: {
1447
+ 'gen_ai.provider.name': 'anthropic',
1448
+ 'gen_ai.request.model': 'gpt-4o', // Same model name, different provider
1449
+ },
1450
+ },
1451
+ ];
1452
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1453
+ const results = await backend.queryLLMEvents({ provider: 'openai', model: 'gpt-4o' });
1454
+ assert.strictEqual(results.length, 1);
1455
+ assert.strictEqual(results[0].attributes?.['gen_ai.provider.name'], 'openai');
1456
+ assert.strictEqual(results[0].attributes?.['gen_ai.request.model'], 'gpt-4o');
1457
+ });
1458
+ it('should return empty array when provider has no events', async () => {
1459
+ const today = getTestDate();
1460
+ const mockEvents = [
1461
+ {
1462
+ timestamp: '2026-01-28T10:00:00Z',
1463
+ name: 'llm.completion',
1464
+ attributes: {
1465
+ 'gen_ai.provider.name': 'anthropic',
1466
+ 'gen_ai.request.model': 'claude-3',
1467
+ },
1468
+ },
1469
+ ];
1470
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1471
+ const results = await backend.queryLLMEvents({ provider: 'nonexistent-provider' });
1472
+ assert.strictEqual(results.length, 0);
1473
+ });
1474
+ it('should filter events by operationName', async () => {
1475
+ const today = getTestDate();
1476
+ const mockEvents = [
1477
+ {
1478
+ timestamp: '2026-01-28T10:00:00Z',
1479
+ name: 'llm.chat',
1480
+ attributes: { 'gen_ai.operation.name': 'chat' },
1481
+ },
1482
+ {
1483
+ timestamp: '2026-01-28T10:01:00Z',
1484
+ name: 'llm.embedding',
1485
+ attributes: { 'gen_ai.operation.name': 'embeddings' },
1486
+ },
1487
+ {
1488
+ timestamp: '2026-01-28T10:02:00Z',
1489
+ name: 'agent.invoke',
1490
+ attributes: { 'gen_ai.operation.name': 'invoke_agent' },
1491
+ },
1492
+ ];
1493
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1494
+ const results = await backend.queryLLMEvents({ operationName: 'chat' });
1495
+ assert.strictEqual(results.length, 1);
1496
+ assert.strictEqual(results[0].name, 'llm.chat');
1497
+ });
1498
+ it('should filter events by conversationId', async () => {
1499
+ const today = getTestDate();
1500
+ const mockEvents = [
1501
+ {
1502
+ timestamp: '2026-01-28T10:00:00Z',
1503
+ name: 'llm.chat',
1504
+ attributes: { 'gen_ai.conversation.id': 'conv-abc123' },
1505
+ },
1506
+ {
1507
+ timestamp: '2026-01-28T10:01:00Z',
1508
+ name: 'llm.chat',
1509
+ attributes: { 'gen_ai.conversation.id': 'conv-xyz789' },
1510
+ },
1511
+ {
1512
+ timestamp: '2026-01-28T10:02:00Z',
1513
+ name: 'llm.chat',
1514
+ attributes: { 'gen_ai.conversation.id': 'conv-abc123' },
1515
+ },
1516
+ ];
1517
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1518
+ const results = await backend.queryLLMEvents({ conversationId: 'conv-abc123' });
1519
+ assert.strictEqual(results.length, 2);
1520
+ });
1521
+ it('should combine OTel GenAI filters with other filters', async () => {
1522
+ const today = getTestDate();
1523
+ const mockEvents = [
1524
+ {
1525
+ timestamp: '2026-01-28T10:00:00Z',
1526
+ name: 'llm.chat',
1527
+ attributes: {
1528
+ 'gen_ai.operation.name': 'chat',
1529
+ 'gen_ai.conversation.id': 'conv-abc123',
1530
+ 'gen_ai.request.model': 'claude-3-opus',
1531
+ },
1532
+ },
1533
+ {
1534
+ timestamp: '2026-01-28T10:01:00Z',
1535
+ name: 'llm.chat',
1536
+ attributes: {
1537
+ 'gen_ai.operation.name': 'chat',
1538
+ 'gen_ai.conversation.id': 'conv-abc123',
1539
+ 'gen_ai.request.model': 'gpt-4',
1540
+ },
1541
+ },
1542
+ ];
1543
+ await writeJsonlFileAsync(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
1544
+ const results = await backend.queryLLMEvents({
1545
+ operationName: 'chat',
1546
+ conversationId: 'conv-abc123',
1547
+ model: 'claude-3-opus',
1548
+ });
1549
+ assert.strictEqual(results.length, 1);
1550
+ assert.strictEqual(results[0].attributes['gen_ai.request.model'], 'claude-3-opus');
1551
+ });
1552
+ });
1553
+ describe('queryTraces OTel GenAI agent/tool filters', () => {
1554
+ it('should filter traces by agentId', async () => {
1555
+ const today = getTestDate();
1556
+ const mockSpans = [
1557
+ {
1558
+ traceId: 'trace1',
1559
+ spanId: 'span1',
1560
+ name: 'agent.invoke',
1561
+ startTime: [1700000000, 0],
1562
+ attributes: { 'gen_ai.agent.id': 'agent-001' },
1563
+ },
1564
+ {
1565
+ traceId: 'trace1',
1566
+ spanId: 'span2',
1567
+ name: 'agent.invoke',
1568
+ startTime: [1700000001, 0],
1569
+ attributes: { 'gen_ai.agent.id': 'agent-002' },
1570
+ },
1571
+ ];
1572
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
1573
+ const results = await backend.queryTraces({ agentId: 'agent-001' });
1574
+ assert.strictEqual(results.length, 1);
1575
+ assert.strictEqual(results[0].attributes?.['gen_ai.agent.id'], 'agent-001');
1576
+ });
1577
+ it('should filter traces by agentName', async () => {
1578
+ const today = getTestDate();
1579
+ const mockSpans = [
1580
+ {
1581
+ traceId: 'trace1',
1582
+ spanId: 'span1',
1583
+ name: 'agent.invoke',
1584
+ startTime: [1700000000, 0],
1585
+ attributes: { 'gen_ai.agent.name': 'Explore' },
1586
+ },
1587
+ {
1588
+ traceId: 'trace1',
1589
+ spanId: 'span2',
1590
+ name: 'agent.invoke',
1591
+ startTime: [1700000001, 0],
1592
+ attributes: { 'gen_ai.agent.name': 'Plan' },
1593
+ },
1594
+ ];
1595
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
1596
+ const results = await backend.queryTraces({ agentName: 'Explore' });
1597
+ assert.strictEqual(results.length, 1);
1598
+ assert.strictEqual(results[0].attributes?.['gen_ai.agent.name'], 'Explore');
1599
+ });
1600
+ it('should filter traces by toolName', async () => {
1601
+ const today = getTestDate();
1602
+ const mockSpans = [
1603
+ {
1604
+ traceId: 'trace1',
1605
+ spanId: 'span1',
1606
+ name: 'tool.execute',
1607
+ startTime: [1700000000, 0],
1608
+ attributes: { 'gen_ai.tool.name': 'Read' },
1609
+ },
1610
+ {
1611
+ traceId: 'trace1',
1612
+ spanId: 'span2',
1613
+ name: 'tool.execute',
1614
+ startTime: [1700000001, 0],
1615
+ attributes: { 'gen_ai.tool.name': 'Write' },
1616
+ },
1617
+ ];
1618
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
1619
+ const results = await backend.queryTraces({ toolName: 'Read' });
1620
+ assert.strictEqual(results.length, 1);
1621
+ assert.strictEqual(results[0].attributes?.['gen_ai.tool.name'], 'Read');
1622
+ });
1623
+ it('should filter traces by toolCallId', async () => {
1624
+ const today = getTestDate();
1625
+ const mockSpans = [
1626
+ {
1627
+ traceId: 'trace1',
1628
+ spanId: 'span1',
1629
+ name: 'tool.execute',
1630
+ startTime: [1700000000, 0],
1631
+ attributes: { 'gen_ai.tool.call.id': 'toolu_abc123' },
1632
+ },
1633
+ {
1634
+ traceId: 'trace1',
1635
+ spanId: 'span2',
1636
+ name: 'tool.execute',
1637
+ startTime: [1700000001, 0],
1638
+ attributes: { 'gen_ai.tool.call.id': 'toolu_xyz789' },
1639
+ },
1640
+ ];
1641
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
1642
+ const results = await backend.queryTraces({ toolCallId: 'toolu_abc123' });
1643
+ assert.strictEqual(results.length, 1);
1644
+ assert.strictEqual(results[0].attributes?.['gen_ai.tool.call.id'], 'toolu_abc123');
1645
+ });
1646
+ it('should filter traces by toolType', async () => {
1647
+ const today = getTestDate();
1648
+ const mockSpans = [
1649
+ {
1650
+ traceId: 'trace1',
1651
+ spanId: 'span1',
1652
+ name: 'tool.execute',
1653
+ startTime: [1700000000, 0],
1654
+ attributes: { 'gen_ai.tool.type': 'function' },
1655
+ },
1656
+ {
1657
+ traceId: 'trace1',
1658
+ spanId: 'span2',
1659
+ name: 'tool.execute',
1660
+ startTime: [1700000001, 0],
1661
+ attributes: { 'gen_ai.tool.type': 'mcp' },
1662
+ },
1663
+ ];
1664
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
1665
+ const results = await backend.queryTraces({ toolType: 'function' });
1666
+ assert.strictEqual(results.length, 1);
1667
+ assert.strictEqual(results[0].attributes?.['gen_ai.tool.type'], 'function');
1668
+ });
1669
+ it('should filter traces by operationName', async () => {
1670
+ const today = getTestDate();
1671
+ const mockSpans = [
1672
+ {
1673
+ traceId: 'trace1',
1674
+ spanId: 'span1',
1675
+ name: 'llm.call',
1676
+ startTime: [1700000000, 0],
1677
+ attributes: { 'gen_ai.operation.name': 'chat' },
1678
+ },
1679
+ {
1680
+ traceId: 'trace1',
1681
+ spanId: 'span2',
1682
+ name: 'tool.execute',
1683
+ startTime: [1700000001, 0],
1684
+ attributes: { 'gen_ai.operation.name': 'execute_tool' },
1685
+ },
1686
+ ];
1687
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
1688
+ const results = await backend.queryTraces({ operationName: 'chat' });
1689
+ assert.strictEqual(results.length, 1);
1690
+ assert.strictEqual(results[0].attributes?.['gen_ai.operation.name'], 'chat');
1691
+ });
1692
+ it('should combine agent/tool filters with other trace filters', async () => {
1693
+ const today = getTestDate();
1694
+ const mockSpans = [
1695
+ {
1696
+ traceId: 'trace1',
1697
+ spanId: 'span1',
1698
+ name: 'agent.explore',
1699
+ startTime: [1700000000, 0],
1700
+ duration: [0, 100000000], // 100ms
1701
+ attributes: {
1702
+ 'gen_ai.agent.name': 'Explore',
1703
+ 'gen_ai.tool.name': 'Grep',
1704
+ },
1705
+ },
1706
+ {
1707
+ traceId: 'trace1',
1708
+ spanId: 'span2',
1709
+ name: 'agent.explore',
1710
+ startTime: [1700000001, 0],
1711
+ duration: [0, 200000000], // 200ms
1712
+ attributes: {
1713
+ 'gen_ai.agent.name': 'Explore',
1714
+ 'gen_ai.tool.name': 'Read',
1715
+ },
1716
+ },
1717
+ ];
1718
+ await writeJsonlFileAsync(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
1719
+ const results = await backend.queryTraces({
1720
+ agentName: 'Explore',
1721
+ toolName: 'Grep',
1722
+ spanName: 'agent',
1723
+ });
1724
+ assert.strictEqual(results.length, 1);
1725
+ assert.strictEqual(results[0].attributes?.['gen_ai.tool.name'], 'Grep');
1726
+ });
1727
+ });
1728
+ });
1729
+ //# sourceMappingURL=local-jsonl-traces.test.js.map