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,876 @@
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 { createMockMetric, createMockMetrics, resetBuilderCounters, } from '../test-helpers/test-data-builders.js';
7
+ describe('LocalJsonlBackend', () => {
8
+ let tempDir;
9
+ let backend;
10
+ before(() => {
11
+ tempDir = getSharedTempDir('LocalJsonlBackend-Metrics');
12
+ });
13
+ beforeEach(() => {
14
+ clearTempDir(tempDir);
15
+ backend = new LocalJsonlBackend(tempDir);
16
+ });
17
+ after(() => {
18
+ removeSharedTempDir('LocalJsonlBackend-Metrics');
19
+ });
20
+ describe('queryMetrics', () => {
21
+ it('should read and normalize metric data points from JSONL files', async () => {
22
+ const today = getTestDate();
23
+ resetBuilderCounters();
24
+ const mockMetrics = [
25
+ createMockMetric({
26
+ name: 'http.requests.total',
27
+ value: 100,
28
+ type: 'counter',
29
+ unit: 'requests',
30
+ resource: { serviceName: 'api-gateway' },
31
+ attributes: { 'http.method': 'GET', 'http.status_code': 200 },
32
+ }),
33
+ ];
34
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
35
+ const results = await backend.queryMetrics({});
36
+ assert.strictEqual(results.length, 1);
37
+ assert.strictEqual(results[0].name, 'http.requests.total');
38
+ assert.strictEqual(results[0].value, 100);
39
+ assert.strictEqual(results[0].unit, 'requests');
40
+ assert.strictEqual(results[0].attributes?.['service.name'], 'api-gateway');
41
+ assert.strictEqual(results[0].attributes?.['http.method'], 'GET');
42
+ });
43
+ it('should filter metrics by name substring', async () => {
44
+ const today = getTestDate();
45
+ resetBuilderCounters();
46
+ const mockMetrics = [
47
+ createMockMetric({ name: 'http.requests.total', value: 100, type: 'counter' }),
48
+ createMockMetric({ name: 'http.request.duration', value: 150, type: 'histogram' }),
49
+ createMockMetric({ name: 'memory.usage', value: 512, type: 'gauge' }),
50
+ ];
51
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
52
+ const results = await backend.queryMetrics({ metricName: 'http' });
53
+ assert.strictEqual(results.length, 2);
54
+ assert.ok(results.every(m => m.name.includes('http')));
55
+ });
56
+ it('should apply limit and offset to metric results', async () => {
57
+ const today = getTestDate();
58
+ resetBuilderCounters();
59
+ const mockMetrics = createMockMetrics(150, (i) => ({
60
+ timestamp: new Date(Date.now() + i * 1000).toISOString(),
61
+ name: `metric.${i}`,
62
+ value: i * 10,
63
+ type: 'gauge',
64
+ }));
65
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
66
+ const results = await backend.queryMetrics({ limit: 50, offset: 30 });
67
+ assert.strictEqual(results.length, 50);
68
+ assert.strictEqual(results[0].name, 'metric.30');
69
+ });
70
+ it('should aggregate metrics with sum function', async () => {
71
+ const today = getTestDate();
72
+ resetBuilderCounters();
73
+ const mockMetrics = [
74
+ createMockMetric({ name: 'requests', value: 100, type: 'counter' }),
75
+ createMockMetric({ name: 'requests', value: 150, type: 'counter' }),
76
+ createMockMetric({ name: 'requests', value: 200, type: 'counter' }),
77
+ ];
78
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
79
+ const results = await backend.queryMetrics({ aggregation: 'sum' });
80
+ assert.strictEqual(results.length, 1);
81
+ assert.strictEqual(results[0].value, 450);
82
+ });
83
+ it('should aggregate metrics with avg function', async () => {
84
+ const today = getTestDate();
85
+ resetBuilderCounters();
86
+ const mockMetrics = [
87
+ createMockMetric({ name: 'latency', value: 100, type: 'histogram' }),
88
+ createMockMetric({ name: 'latency', value: 200, type: 'histogram' }),
89
+ createMockMetric({ name: 'latency', value: 300, type: 'histogram' }),
90
+ ];
91
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
92
+ const results = await backend.queryMetrics({ aggregation: 'avg' });
93
+ assert.strictEqual(results.length, 1);
94
+ assert.strictEqual(results[0].value, 200);
95
+ });
96
+ it('should aggregate metrics with min function', async () => {
97
+ const today = getTestDate();
98
+ resetBuilderCounters();
99
+ const mockMetrics = [
100
+ createMockMetric({ name: 'response_time', value: 150, type: 'gauge' }),
101
+ createMockMetric({ name: 'response_time', value: 50, type: 'gauge' }),
102
+ createMockMetric({ name: 'response_time', value: 200, type: 'gauge' }),
103
+ ];
104
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
105
+ const results = await backend.queryMetrics({ aggregation: 'min' });
106
+ assert.strictEqual(results.length, 1);
107
+ assert.strictEqual(results[0].value, 50);
108
+ });
109
+ it('should aggregate metrics with max function', async () => {
110
+ const today = getTestDate();
111
+ resetBuilderCounters();
112
+ const mockMetrics = [
113
+ createMockMetric({ name: 'memory', value: 512, type: 'gauge' }),
114
+ createMockMetric({ name: 'memory', value: 256, type: 'gauge' }),
115
+ createMockMetric({ name: 'memory', value: 1024, type: 'gauge' }),
116
+ ];
117
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
118
+ const results = await backend.queryMetrics({ aggregation: 'max' });
119
+ assert.strictEqual(results.length, 1);
120
+ assert.strictEqual(results[0].value, 1024);
121
+ });
122
+ it('should aggregate metrics with count function', async () => {
123
+ const today = getTestDate();
124
+ resetBuilderCounters();
125
+ const mockMetrics = [
126
+ createMockMetric({ name: 'events', value: 10, type: 'counter' }),
127
+ createMockMetric({ name: 'events', value: 20, type: 'counter' }),
128
+ createMockMetric({ name: 'events', value: 30, type: 'counter' }),
129
+ ];
130
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
131
+ const results = await backend.queryMetrics({ aggregation: 'count' });
132
+ assert.strictEqual(results.length, 1);
133
+ assert.strictEqual(results[0].value, 3);
134
+ });
135
+ it('should aggregate metrics with p50 (median) function', async () => {
136
+ const today = getTestDate();
137
+ resetBuilderCounters();
138
+ const mockMetrics = [
139
+ createMockMetric({ name: 'latency', value: 10, type: 'histogram' }),
140
+ createMockMetric({ name: 'latency', value: 20, type: 'histogram' }),
141
+ createMockMetric({ name: 'latency', value: 30, type: 'histogram' }),
142
+ createMockMetric({ name: 'latency', value: 40, type: 'histogram' }),
143
+ createMockMetric({ name: 'latency', value: 50, type: 'histogram' }),
144
+ ];
145
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
146
+ const results = await backend.queryMetrics({ aggregation: 'p50' });
147
+ assert.strictEqual(results.length, 1);
148
+ assert.strictEqual(results[0].value, 30); // median of [10, 20, 30, 40, 50]
149
+ });
150
+ it('should aggregate metrics with p95 function', async () => {
151
+ const today = getTestDate();
152
+ resetBuilderCounters();
153
+ // Create 100 data points for a more realistic p95 calculation
154
+ const mockMetrics = createMockMetrics(100, (i) => ({
155
+ timestamp: `2026-01-28T10:${String(Math.floor(i / 60)).padStart(2, '0')}:${String(i % 60).padStart(2, '0')}Z`,
156
+ name: 'response_time',
157
+ value: i + 1, // values 1-100
158
+ type: 'histogram',
159
+ }));
160
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
161
+ const results = await backend.queryMetrics({ aggregation: 'p95' });
162
+ assert.strictEqual(results.length, 1);
163
+ assert.strictEqual(results[0].value, 95); // 95th percentile of 1-100
164
+ });
165
+ it('should aggregate metrics with p99 function', async () => {
166
+ const today = getTestDate();
167
+ resetBuilderCounters();
168
+ // Create 100 data points for a more realistic p99 calculation
169
+ const mockMetrics = createMockMetrics(100, (i) => ({
170
+ timestamp: `2026-01-28T10:${String(Math.floor(i / 60)).padStart(2, '0')}:${String(i % 60).padStart(2, '0')}Z`,
171
+ name: 'response_time',
172
+ value: i + 1, // values 1-100
173
+ type: 'histogram',
174
+ }));
175
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
176
+ const results = await backend.queryMetrics({ aggregation: 'p99' });
177
+ assert.strictEqual(results.length, 1);
178
+ assert.strictEqual(results[0].value, 99); // 99th percentile of 1-100
179
+ });
180
+ it('should handle p50 with even number of values', async () => {
181
+ const today = getTestDate();
182
+ resetBuilderCounters();
183
+ const mockMetrics = [
184
+ createMockMetric({ name: 'latency', value: 10, type: 'histogram' }),
185
+ createMockMetric({ name: 'latency', value: 20, type: 'histogram' }),
186
+ createMockMetric({ name: 'latency', value: 30, type: 'histogram' }),
187
+ createMockMetric({ name: 'latency', value: 40, type: 'histogram' }),
188
+ ];
189
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
190
+ const results = await backend.queryMetrics({ aggregation: 'p50' });
191
+ assert.strictEqual(results.length, 1);
192
+ assert.strictEqual(results[0].value, 20); // ceil(0.5 * 4) - 1 = 1, sorted[1] = 20
193
+ });
194
+ it('should handle percentile with single value', async () => {
195
+ const today = getTestDate();
196
+ resetBuilderCounters();
197
+ const mockMetrics = [
198
+ createMockMetric({ name: 'latency', value: 42, type: 'histogram' }),
199
+ ];
200
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
201
+ const results = await backend.queryMetrics({ aggregation: 'p95' });
202
+ assert.strictEqual(results.length, 1);
203
+ assert.strictEqual(results[0].value, 42); // single value is the only percentile
204
+ });
205
+ it('should calculate percentiles with groupBy', async () => {
206
+ const today = getTestDate();
207
+ resetBuilderCounters();
208
+ const mockMetrics = [
209
+ createMockMetric({ name: 'latency', value: 10, type: 'histogram', attributes: { endpoint: '/api/users' } }),
210
+ createMockMetric({ name: 'latency', value: 20, type: 'histogram', attributes: { endpoint: '/api/users' } }),
211
+ createMockMetric({ name: 'latency', value: 30, type: 'histogram', attributes: { endpoint: '/api/users' } }),
212
+ createMockMetric({ name: 'latency', value: 100, type: 'histogram', attributes: { endpoint: '/api/orders' } }),
213
+ createMockMetric({ name: 'latency', value: 200, type: 'histogram', attributes: { endpoint: '/api/orders' } }),
214
+ createMockMetric({ name: 'latency', value: 300, type: 'histogram', attributes: { endpoint: '/api/orders' } }),
215
+ ];
216
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
217
+ const results = await backend.queryMetrics({ aggregation: 'p50', groupBy: ['endpoint'] });
218
+ assert.strictEqual(results.length, 2);
219
+ const usersMetric = results.find(m => m.attributes?.endpoint === '/api/users');
220
+ const ordersMetric = results.find(m => m.attributes?.endpoint === '/api/orders');
221
+ assert.strictEqual(usersMetric?.value, 20); // median of [10, 20, 30]
222
+ assert.strictEqual(ordersMetric?.value, 200); // median of [100, 200, 300]
223
+ });
224
+ it('should aggregate metrics grouped by attributes', async () => {
225
+ const today = getTestDate();
226
+ resetBuilderCounters();
227
+ const mockMetrics = [
228
+ createMockMetric({ name: 'http.requests', value: 100, type: 'counter', attributes: { method: 'GET' } }),
229
+ createMockMetric({ name: 'http.requests', value: 50, type: 'counter', attributes: { method: 'POST' } }),
230
+ createMockMetric({ name: 'http.requests', value: 200, type: 'counter', attributes: { method: 'GET' } }),
231
+ ];
232
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
233
+ const results = await backend.queryMetrics({ aggregation: 'sum', groupBy: ['method'] });
234
+ assert.strictEqual(results.length, 2);
235
+ const getMetric = results.find(m => m.attributes?.method === 'GET');
236
+ const postMetric = results.find(m => m.attributes?.method === 'POST');
237
+ assert.strictEqual(getMetric?.value, 300);
238
+ assert.strictEqual(postMetric?.value, 50);
239
+ });
240
+ it('should return empty array when no metrics found', async () => {
241
+ // No files created
242
+ const results = await backend.queryMetrics({});
243
+ assert.strictEqual(results.length, 0);
244
+ });
245
+ it('should aggregate metrics by time bucket with 1m buckets', async () => {
246
+ const today = getTestDate();
247
+ const mockMetrics = [
248
+ // First minute bucket: 10:00:00 - 10:00:59
249
+ { timestamp: `${today}T10:00:00Z`, name: 'requests', value: 10, type: 'counter' },
250
+ { timestamp: `${today}T10:00:30Z`, name: 'requests', value: 20, type: 'counter' },
251
+ { timestamp: `${today}T10:00:45Z`, name: 'requests', value: 30, type: 'counter' },
252
+ // Second minute bucket: 10:01:00 - 10:01:59
253
+ { timestamp: `${today}T10:01:00Z`, name: 'requests', value: 100, type: 'counter' },
254
+ { timestamp: `${today}T10:01:30Z`, name: 'requests', value: 200, type: 'counter' },
255
+ // Third minute bucket: 10:02:00 - 10:02:59
256
+ { timestamp: `${today}T10:02:15Z`, name: 'requests', value: 50, type: 'counter' },
257
+ ];
258
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
259
+ const results = await backend.queryMetrics({ aggregation: 'sum', timeBucket: '1m' });
260
+ assert.strictEqual(results.length, 3);
261
+ // Results should be sorted by timestamp
262
+ assert.strictEqual(results[0].value, 60); // 10 + 20 + 30
263
+ assert.strictEqual(results[1].value, 300); // 100 + 200
264
+ assert.strictEqual(results[2].value, 50); // 50
265
+ // Timestamps should be floored to bucket boundaries
266
+ assert.strictEqual(results[0].timestamp, `${today}T10:00:00.000Z`);
267
+ assert.strictEqual(results[1].timestamp, `${today}T10:01:00.000Z`);
268
+ assert.strictEqual(results[2].timestamp, `${today}T10:02:00.000Z`);
269
+ });
270
+ it('should aggregate metrics by time bucket with 5m buckets', async () => {
271
+ const today = getTestDate();
272
+ const mockMetrics = [
273
+ // First 5-minute bucket: 10:00:00 - 10:04:59
274
+ { timestamp: `${today}T10:00:00Z`, name: 'requests', value: 10, type: 'counter' },
275
+ { timestamp: `${today}T10:02:00Z`, name: 'requests', value: 20, type: 'counter' },
276
+ { timestamp: `${today}T10:04:00Z`, name: 'requests', value: 30, type: 'counter' },
277
+ // Second 5-minute bucket: 10:05:00 - 10:09:59
278
+ { timestamp: `${today}T10:05:00Z`, name: 'requests', value: 100, type: 'counter' },
279
+ { timestamp: `${today}T10:08:00Z`, name: 'requests', value: 200, type: 'counter' },
280
+ ];
281
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
282
+ const results = await backend.queryMetrics({ aggregation: 'sum', timeBucket: '5m' });
283
+ assert.strictEqual(results.length, 2);
284
+ assert.strictEqual(results[0].value, 60); // 10 + 20 + 30
285
+ assert.strictEqual(results[1].value, 300); // 100 + 200
286
+ });
287
+ it('should aggregate metrics by time bucket with 1h buckets', async () => {
288
+ const today = getTestDate();
289
+ const mockMetrics = [
290
+ // First hour bucket: 10:00:00 - 10:59:59
291
+ { timestamp: `${today}T10:00:00Z`, name: 'requests', value: 10, type: 'counter' },
292
+ { timestamp: `${today}T10:30:00Z`, name: 'requests', value: 20, type: 'counter' },
293
+ // Second hour bucket: 11:00:00 - 11:59:59
294
+ { timestamp: `${today}T11:00:00Z`, name: 'requests', value: 100, type: 'counter' },
295
+ { timestamp: `${today}T11:45:00Z`, name: 'requests', value: 200, type: 'counter' },
296
+ ];
297
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
298
+ const results = await backend.queryMetrics({ aggregation: 'sum', timeBucket: '1h' });
299
+ assert.strictEqual(results.length, 2);
300
+ assert.strictEqual(results[0].value, 30); // 10 + 20
301
+ assert.strictEqual(results[1].value, 300); // 100 + 200
302
+ });
303
+ it('should aggregate metrics by time bucket with 1d buckets', async () => {
304
+ // Create metrics files for two days
305
+ await writeJsonlFileAsync(path.join(tempDir, 'metrics-2026-01-28.jsonl'), [
306
+ { timestamp: '2026-01-28T10:00:00Z', name: 'requests', value: 100, type: 'counter' },
307
+ { timestamp: '2026-01-28T20:00:00Z', name: 'requests', value: 200, type: 'counter' },
308
+ ]);
309
+ await writeJsonlFileAsync(path.join(tempDir, 'metrics-2026-01-29.jsonl'), [
310
+ { timestamp: '2026-01-29T08:00:00Z', name: 'requests', value: 300, type: 'counter' },
311
+ { timestamp: '2026-01-29T16:00:00Z', name: 'requests', value: 400, type: 'counter' },
312
+ ]);
313
+ const results = await backend.queryMetrics({
314
+ aggregation: 'sum',
315
+ timeBucket: '1d',
316
+ startDate: '2026-01-28',
317
+ endDate: '2026-01-29',
318
+ });
319
+ assert.strictEqual(results.length, 2);
320
+ assert.strictEqual(results[0].value, 300); // 100 + 200
321
+ assert.strictEqual(results[1].value, 700); // 300 + 400
322
+ });
323
+ it('should combine time bucket with groupBy', async () => {
324
+ const today = getTestDate();
325
+ const mockMetrics = [
326
+ // First minute, method=GET
327
+ { timestamp: `${today}T10:00:00Z`, name: 'requests', value: 10, type: 'counter', attributes: { method: 'GET' } },
328
+ { timestamp: `${today}T10:00:30Z`, name: 'requests', value: 20, type: 'counter', attributes: { method: 'GET' } },
329
+ // First minute, method=POST
330
+ { timestamp: `${today}T10:00:15Z`, name: 'requests', value: 5, type: 'counter', attributes: { method: 'POST' } },
331
+ // Second minute, method=GET
332
+ { timestamp: `${today}T10:01:00Z`, name: 'requests', value: 100, type: 'counter', attributes: { method: 'GET' } },
333
+ // Second minute, method=POST
334
+ { timestamp: `${today}T10:01:30Z`, name: 'requests', value: 50, type: 'counter', attributes: { method: 'POST' } },
335
+ ];
336
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
337
+ const results = await backend.queryMetrics({
338
+ aggregation: 'sum',
339
+ timeBucket: '1m',
340
+ groupBy: ['method'],
341
+ });
342
+ assert.strictEqual(results.length, 4);
343
+ // First bucket GET
344
+ const bucket1GET = results.find(m => m.timestamp === `${today}T10:00:00.000Z` && m.attributes?.method === 'GET');
345
+ assert.strictEqual(bucket1GET?.value, 30); // 10 + 20
346
+ // First bucket POST
347
+ const bucket1POST = results.find(m => m.timestamp === `${today}T10:00:00.000Z` && m.attributes?.method === 'POST');
348
+ assert.strictEqual(bucket1POST?.value, 5);
349
+ // Second bucket GET
350
+ const bucket2GET = results.find(m => m.timestamp === `${today}T10:01:00.000Z` && m.attributes?.method === 'GET');
351
+ assert.strictEqual(bucket2GET?.value, 100);
352
+ // Second bucket POST
353
+ const bucket2POST = results.find(m => m.timestamp === `${today}T10:01:00.000Z` && m.attributes?.method === 'POST');
354
+ assert.strictEqual(bucket2POST?.value, 50);
355
+ });
356
+ it('should use avg aggregation with time buckets', async () => {
357
+ const today = getTestDate();
358
+ const mockMetrics = [
359
+ // First minute bucket
360
+ { timestamp: `${today}T10:00:00Z`, name: 'latency', value: 100, type: 'histogram' },
361
+ { timestamp: `${today}T10:00:30Z`, name: 'latency', value: 200, type: 'histogram' },
362
+ { timestamp: `${today}T10:00:45Z`, name: 'latency', value: 300, type: 'histogram' },
363
+ // Second minute bucket
364
+ { timestamp: `${today}T10:01:00Z`, name: 'latency', value: 500, type: 'histogram' },
365
+ { timestamp: `${today}T10:01:30Z`, name: 'latency', value: 700, type: 'histogram' },
366
+ ];
367
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
368
+ const results = await backend.queryMetrics({ aggregation: 'avg', timeBucket: '1m' });
369
+ assert.strictEqual(results.length, 2);
370
+ assert.strictEqual(results[0].value, 200); // (100 + 200 + 300) / 3
371
+ assert.strictEqual(results[1].value, 600); // (500 + 700) / 2
372
+ });
373
+ it('should ignore invalid time bucket format', async () => {
374
+ const today = getTestDate();
375
+ const mockMetrics = [
376
+ { timestamp: `${today}T10:00:00Z`, name: 'requests', value: 10, type: 'counter' },
377
+ { timestamp: `${today}T10:00:30Z`, name: 'requests', value: 20, type: 'counter' },
378
+ { timestamp: `${today}T10:01:00Z`, name: 'requests', value: 30, type: 'counter' },
379
+ ];
380
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
381
+ // Invalid format - should be ignored and aggregate all together
382
+ const results = await backend.queryMetrics({ aggregation: 'sum', timeBucket: 'invalid' });
383
+ assert.strictEqual(results.length, 1);
384
+ assert.strictEqual(results[0].value, 60); // All grouped together
385
+ });
386
+ it('should calculate rate of change per second', async () => {
387
+ const today = getTestDate();
388
+ const mockMetrics = [
389
+ { timestamp: `${today}T10:00:00Z`, name: 'requests', value: 100, type: 'counter' },
390
+ { timestamp: `${today}T10:00:30Z`, name: 'requests', value: 200, type: 'counter' },
391
+ { timestamp: `${today}T10:01:00Z`, name: 'requests', value: 400, type: 'counter' },
392
+ ];
393
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
394
+ const results = await backend.queryMetrics({ aggregation: 'rate' });
395
+ assert.strictEqual(results.length, 1);
396
+ // Rate = (400 - 100) / 60 seconds = 5 per second
397
+ assert.strictEqual(results[0].value, 5);
398
+ });
399
+ it('should calculate rate with timeBucket', async () => {
400
+ const today = getTestDate();
401
+ const mockMetrics = [
402
+ // First minute bucket: 10:00:00 - 10:00:59
403
+ { timestamp: `${today}T10:00:00Z`, name: 'requests', value: 0, type: 'counter' },
404
+ { timestamp: `${today}T10:00:30Z`, name: 'requests', value: 60, type: 'counter' },
405
+ // Second minute bucket: 10:01:00 - 10:01:59
406
+ { timestamp: `${today}T10:01:00Z`, name: 'requests', value: 100, type: 'counter' },
407
+ { timestamp: `${today}T10:01:30Z`, name: 'requests', value: 250, type: 'counter' },
408
+ ];
409
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
410
+ const results = await backend.queryMetrics({ aggregation: 'rate', timeBucket: '1m' });
411
+ assert.strictEqual(results.length, 2);
412
+ // First bucket: (60 - 0) / 30 seconds = 2 per second
413
+ assert.strictEqual(results[0].value, 2);
414
+ // Second bucket: (250 - 100) / 30 seconds = 5 per second
415
+ assert.strictEqual(results[1].value, 5);
416
+ });
417
+ it('should return rate of 0 for single data point', async () => {
418
+ const today = getTestDate();
419
+ const mockMetrics = [
420
+ { timestamp: `${today}T10:00:00Z`, name: 'requests', value: 100, type: 'counter' },
421
+ ];
422
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
423
+ const results = await backend.queryMetrics({ aggregation: 'rate' });
424
+ assert.strictEqual(results.length, 1);
425
+ assert.strictEqual(results[0].value, 0);
426
+ });
427
+ it('should return rate of 0 when all timestamps are the same', async () => {
428
+ const today = getTestDate();
429
+ const mockMetrics = [
430
+ { timestamp: `${today}T10:00:00Z`, name: 'requests', value: 100, type: 'counter' },
431
+ { timestamp: `${today}T10:00:00Z`, name: 'requests', value: 200, type: 'counter' },
432
+ { timestamp: `${today}T10:00:00Z`, name: 'requests', value: 300, type: 'counter' },
433
+ ];
434
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
435
+ const results = await backend.queryMetrics({ aggregation: 'rate' });
436
+ assert.strictEqual(results.length, 1);
437
+ // Avoid division by zero - return 0 when duration is 0
438
+ assert.strictEqual(results[0].value, 0);
439
+ });
440
+ it('should read and normalize histogram metrics with bucket distribution', async () => {
441
+ const today = getTestDate();
442
+ const mockMetrics = [
443
+ {
444
+ timestamp: '2026-01-28T10:00:00Z',
445
+ name: 'http.request.duration',
446
+ value: 150, // typically the sum/count average or similar aggregate
447
+ type: 'histogram',
448
+ unit: 'ms',
449
+ resource: { serviceName: 'api-gateway' },
450
+ attributes: { 'http.method': 'GET' },
451
+ histogram: {
452
+ buckets: [
453
+ { le: 50, count: 10 },
454
+ { le: 100, count: 25 },
455
+ { le: 250, count: 45 },
456
+ { le: 500, count: 48 },
457
+ { le: Infinity, count: 50 },
458
+ ],
459
+ sum: 7500,
460
+ count: 50,
461
+ },
462
+ },
463
+ ];
464
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
465
+ const results = await backend.queryMetrics({});
466
+ assert.strictEqual(results.length, 1);
467
+ assert.strictEqual(results[0].name, 'http.request.duration');
468
+ assert.strictEqual(results[0].value, 150);
469
+ assert.strictEqual(results[0].unit, 'ms');
470
+ assert.strictEqual(results[0].attributes?.['service.name'], 'api-gateway');
471
+ assert.strictEqual(results[0].attributes?.['http.method'], 'GET');
472
+ // Verify histogram data is present
473
+ assert.ok(results[0].histogram, 'Histogram data should be present');
474
+ assert.strictEqual(results[0].histogram?.sum, 7500);
475
+ assert.strictEqual(results[0].histogram?.count, 50);
476
+ assert.strictEqual(results[0].histogram?.buckets.length, 5);
477
+ // Verify bucket boundaries and cumulative counts
478
+ assert.strictEqual(results[0].histogram?.buckets[0].le, 50);
479
+ assert.strictEqual(results[0].histogram?.buckets[0].count, 10);
480
+ assert.strictEqual(results[0].histogram?.buckets[1].le, 100);
481
+ assert.strictEqual(results[0].histogram?.buckets[1].count, 25);
482
+ assert.strictEqual(results[0].histogram?.buckets[2].le, 250);
483
+ assert.strictEqual(results[0].histogram?.buckets[2].count, 45);
484
+ });
485
+ it('should handle histogram metrics without histogram data (non-histogram type)', async () => {
486
+ const today = getTestDate();
487
+ const mockMetrics = [
488
+ {
489
+ timestamp: '2026-01-28T10:00:00Z',
490
+ name: 'http.requests.total',
491
+ value: 100,
492
+ type: 'counter',
493
+ unit: 'requests',
494
+ },
495
+ ];
496
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
497
+ const results = await backend.queryMetrics({});
498
+ assert.strictEqual(results.length, 1);
499
+ assert.strictEqual(results[0].name, 'http.requests.total');
500
+ assert.strictEqual(results[0].value, 100);
501
+ assert.strictEqual(results[0].histogram, undefined);
502
+ });
503
+ it('should handle mixed metric types including histograms', async () => {
504
+ const today = getTestDate();
505
+ const mockMetrics = [
506
+ {
507
+ timestamp: '2026-01-28T10:00:00Z',
508
+ name: 'http.requests.total',
509
+ value: 100,
510
+ type: 'counter',
511
+ },
512
+ {
513
+ timestamp: '2026-01-28T10:00:00Z',
514
+ name: 'http.request.duration',
515
+ value: 150,
516
+ type: 'histogram',
517
+ histogram: {
518
+ buckets: [
519
+ { le: 100, count: 20 },
520
+ { le: 500, count: 80 },
521
+ { le: Infinity, count: 100 },
522
+ ],
523
+ sum: 15000,
524
+ count: 100,
525
+ },
526
+ },
527
+ {
528
+ timestamp: '2026-01-28T10:00:00Z',
529
+ name: 'memory.usage',
530
+ value: 512,
531
+ type: 'gauge',
532
+ },
533
+ ];
534
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
535
+ const results = await backend.queryMetrics({});
536
+ assert.strictEqual(results.length, 3);
537
+ const counter = results.find(m => m.name === 'http.requests.total');
538
+ const histogram = results.find(m => m.name === 'http.request.duration');
539
+ const gauge = results.find(m => m.name === 'memory.usage');
540
+ assert.ok(counter, 'Counter metric should be present');
541
+ assert.strictEqual(counter?.histogram, undefined);
542
+ assert.ok(histogram, 'Histogram metric should be present');
543
+ assert.ok(histogram?.histogram, 'Histogram should have histogram data');
544
+ assert.strictEqual(histogram?.histogram?.count, 100);
545
+ assert.ok(gauge, 'Gauge metric should be present');
546
+ assert.strictEqual(gauge?.histogram, undefined);
547
+ });
548
+ it('should preserve histogram data when filtering by metric name', async () => {
549
+ const today = getTestDate();
550
+ const mockMetrics = [
551
+ {
552
+ timestamp: '2026-01-28T10:00:00Z',
553
+ name: 'api.latency',
554
+ value: 200,
555
+ type: 'histogram',
556
+ histogram: {
557
+ buckets: [
558
+ { le: 100, count: 5 },
559
+ { le: 500, count: 15 },
560
+ { le: 1000, count: 20 },
561
+ ],
562
+ sum: 4000,
563
+ count: 20,
564
+ },
565
+ },
566
+ {
567
+ timestamp: '2026-01-28T10:01:00Z',
568
+ name: 'db.query.duration',
569
+ value: 50,
570
+ type: 'histogram',
571
+ histogram: {
572
+ buckets: [
573
+ { le: 10, count: 30 },
574
+ { le: 50, count: 80 },
575
+ { le: 100, count: 100 },
576
+ ],
577
+ sum: 3500,
578
+ count: 100,
579
+ },
580
+ },
581
+ ];
582
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
583
+ const results = await backend.queryMetrics({ metricName: 'api.latency' });
584
+ assert.strictEqual(results.length, 1);
585
+ assert.strictEqual(results[0].name, 'api.latency');
586
+ assert.ok(results[0].histogram);
587
+ assert.strictEqual(results[0].histogram?.sum, 4000);
588
+ assert.strictEqual(results[0].histogram?.count, 20);
589
+ assert.strictEqual(results[0].histogram?.buckets.length, 3);
590
+ });
591
+ it('should ignore histogram field when metric type is not histogram', async () => {
592
+ const today = getTestDate();
593
+ // Edge case: a metric that has histogram field but type is not 'histogram'
594
+ const mockMetrics = [
595
+ {
596
+ timestamp: '2026-01-28T10:00:00Z',
597
+ name: 'malformed.metric',
598
+ value: 100,
599
+ type: 'gauge', // Not histogram type
600
+ histogram: {
601
+ // This should be ignored since type != 'histogram'
602
+ buckets: [{ le: 100, count: 10 }],
603
+ sum: 500,
604
+ count: 10,
605
+ },
606
+ },
607
+ ];
608
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
609
+ const results = await backend.queryMetrics({});
610
+ assert.strictEqual(results.length, 1);
611
+ assert.strictEqual(results[0].name, 'malformed.metric');
612
+ assert.strictEqual(results[0].value, 100);
613
+ // Histogram data should NOT be included since type is 'gauge', not 'histogram'
614
+ assert.strictEqual(results[0].histogram, undefined);
615
+ });
616
+ it('should normalize aggregationTemporality from numeric OTel values', async () => {
617
+ const today = getTestDate();
618
+ const mockMetrics = [
619
+ {
620
+ timestamp: '2026-01-28T10:00:00Z',
621
+ name: 'http.requests.delta',
622
+ value: 100,
623
+ type: 'counter',
624
+ aggregationTemporality: 1, // DELTA
625
+ },
626
+ {
627
+ timestamp: '2026-01-28T10:01:00Z',
628
+ name: 'http.requests.cumulative',
629
+ value: 500,
630
+ type: 'counter',
631
+ aggregationTemporality: 2, // CUMULATIVE
632
+ },
633
+ {
634
+ timestamp: '2026-01-28T10:02:00Z',
635
+ name: 'http.requests.unspecified',
636
+ value: 50,
637
+ type: 'counter',
638
+ aggregationTemporality: 0, // UNSPECIFIED
639
+ },
640
+ ];
641
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
642
+ const results = await backend.queryMetrics({});
643
+ assert.strictEqual(results.length, 3);
644
+ const deltaMetric = results.find(m => m.name === 'http.requests.delta');
645
+ const cumulativeMetric = results.find(m => m.name === 'http.requests.cumulative');
646
+ const unspecifiedMetric = results.find(m => m.name === 'http.requests.unspecified');
647
+ assert.strictEqual(deltaMetric?.aggregationTemporality, 'DELTA');
648
+ assert.strictEqual(cumulativeMetric?.aggregationTemporality, 'CUMULATIVE');
649
+ assert.strictEqual(unspecifiedMetric?.aggregationTemporality, 'UNSPECIFIED');
650
+ });
651
+ it('should normalize aggregationTemporality from string values', async () => {
652
+ const today = getTestDate();
653
+ const mockMetrics = [
654
+ {
655
+ timestamp: '2026-01-28T10:00:00Z',
656
+ name: 'requests.delta',
657
+ value: 100,
658
+ type: 'counter',
659
+ aggregationTemporality: 'delta', // lowercase
660
+ },
661
+ {
662
+ timestamp: '2026-01-28T10:01:00Z',
663
+ name: 'requests.cumulative',
664
+ value: 500,
665
+ type: 'counter',
666
+ aggregationTemporality: 'CUMULATIVE', // uppercase
667
+ },
668
+ {
669
+ timestamp: '2026-01-28T10:02:00Z',
670
+ name: 'requests.unspecified',
671
+ value: 50,
672
+ type: 'counter',
673
+ aggregationTemporality: 'Unspecified', // mixed case
674
+ },
675
+ ];
676
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
677
+ const results = await backend.queryMetrics({});
678
+ assert.strictEqual(results.length, 3);
679
+ const deltaMetric = results.find(m => m.name === 'requests.delta');
680
+ const cumulativeMetric = results.find(m => m.name === 'requests.cumulative');
681
+ const unspecifiedMetric = results.find(m => m.name === 'requests.unspecified');
682
+ assert.strictEqual(deltaMetric?.aggregationTemporality, 'DELTA');
683
+ assert.strictEqual(cumulativeMetric?.aggregationTemporality, 'CUMULATIVE');
684
+ assert.strictEqual(unspecifiedMetric?.aggregationTemporality, 'UNSPECIFIED');
685
+ });
686
+ it('should return undefined aggregationTemporality when not provided', async () => {
687
+ const today = getTestDate();
688
+ const mockMetrics = [
689
+ {
690
+ timestamp: '2026-01-28T10:00:00Z',
691
+ name: 'gauge.metric',
692
+ value: 42,
693
+ type: 'gauge',
694
+ // No aggregationTemporality field
695
+ },
696
+ ];
697
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
698
+ const results = await backend.queryMetrics({});
699
+ assert.strictEqual(results.length, 1);
700
+ assert.strictEqual(results[0].aggregationTemporality, undefined);
701
+ });
702
+ it('should handle invalid aggregationTemporality string values', async () => {
703
+ const today = getTestDate();
704
+ const mockMetrics = [
705
+ {
706
+ timestamp: '2026-01-28T10:00:00Z',
707
+ name: 'metric.invalid',
708
+ value: 100,
709
+ type: 'counter',
710
+ aggregationTemporality: 'invalid_value',
711
+ },
712
+ ];
713
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
714
+ const results = await backend.queryMetrics({});
715
+ assert.strictEqual(results.length, 1);
716
+ // Invalid values should normalize to UNSPECIFIED
717
+ assert.strictEqual(results[0].aggregationTemporality, 'UNSPECIFIED');
718
+ });
719
+ it('should handle unknown numeric aggregationTemporality values', async () => {
720
+ const today = getTestDate();
721
+ const mockMetrics = [
722
+ {
723
+ timestamp: '2026-01-28T10:00:00Z',
724
+ name: 'metric.unknown',
725
+ value: 100,
726
+ type: 'counter',
727
+ aggregationTemporality: 99, // Unknown numeric value
728
+ },
729
+ ];
730
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
731
+ const results = await backend.queryMetrics({});
732
+ assert.strictEqual(results.length, 1);
733
+ // Unknown numeric values should normalize to UNSPECIFIED
734
+ assert.strictEqual(results[0].aggregationTemporality, 'UNSPECIFIED');
735
+ });
736
+ it('should read and normalize metrics with exemplars', async () => {
737
+ const today = getTestDate();
738
+ const mockMetrics = [
739
+ {
740
+ timestamp: '2026-01-28T10:00:00Z',
741
+ name: 'http.request.duration',
742
+ value: 150,
743
+ type: 'histogram',
744
+ unit: 'ms',
745
+ histogram: {
746
+ buckets: [
747
+ { le: 100, count: 10 },
748
+ { le: 500, count: 45 },
749
+ { le: Infinity, count: 50 },
750
+ ],
751
+ sum: 7500,
752
+ count: 50,
753
+ },
754
+ exemplars: [
755
+ {
756
+ timestamp: '2026-01-28T10:00:00.123Z',
757
+ value: 450,
758
+ traceId: 'abc123def456',
759
+ spanId: 'span789',
760
+ attributes: { 'http.status_code': 500 },
761
+ },
762
+ {
763
+ timestamp: '2026-01-28T10:00:00.456Z',
764
+ value: 95,
765
+ traceId: 'xyz789abc123',
766
+ spanId: 'span456',
767
+ },
768
+ ],
769
+ },
770
+ ];
771
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
772
+ const results = await backend.queryMetrics({});
773
+ assert.strictEqual(results.length, 1);
774
+ assert.strictEqual(results[0].name, 'http.request.duration');
775
+ assert.ok(results[0].exemplars, 'Exemplars should be present');
776
+ assert.strictEqual(results[0].exemplars?.length, 2);
777
+ // Verify first exemplar (high latency with error)
778
+ const highLatencyExemplar = results[0].exemplars?.[0];
779
+ assert.strictEqual(highLatencyExemplar?.value, 450);
780
+ assert.strictEqual(highLatencyExemplar?.traceId, 'abc123def456');
781
+ assert.strictEqual(highLatencyExemplar?.spanId, 'span789');
782
+ assert.strictEqual(highLatencyExemplar?.attributes?.['http.status_code'], 500);
783
+ // Verify second exemplar
784
+ const normalExemplar = results[0].exemplars?.[1];
785
+ assert.strictEqual(normalExemplar?.value, 95);
786
+ assert.strictEqual(normalExemplar?.traceId, 'xyz789abc123');
787
+ });
788
+ it('should normalize exemplar timestamps from [seconds, nanoseconds] format', async () => {
789
+ const today = getTestDate();
790
+ const mockMetrics = [
791
+ {
792
+ timestamp: '2026-01-28T10:00:00Z',
793
+ name: 'api.latency',
794
+ value: 200,
795
+ type: 'histogram',
796
+ exemplars: [
797
+ {
798
+ timestamp: [1738062000, 123000000], // [seconds, nanoseconds]
799
+ value: 350,
800
+ traceId: 'trace123',
801
+ spanId: 'span456',
802
+ },
803
+ ],
804
+ },
805
+ ];
806
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
807
+ const results = await backend.queryMetrics({});
808
+ assert.strictEqual(results.length, 1);
809
+ assert.ok(results[0].exemplars);
810
+ assert.strictEqual(results[0].exemplars?.length, 1);
811
+ // Timestamp should be converted to ISO string
812
+ assert.ok(results[0].exemplars?.[0].timestamp.includes('T'));
813
+ assert.strictEqual(results[0].exemplars?.[0].value, 350);
814
+ });
815
+ it('should handle exemplars without optional fields', async () => {
816
+ const today = getTestDate();
817
+ const mockMetrics = [
818
+ {
819
+ timestamp: '2026-01-28T10:00:00Z',
820
+ name: 'counter.metric',
821
+ value: 100,
822
+ type: 'counter',
823
+ exemplars: [
824
+ {
825
+ value: 1, // Only required field
826
+ },
827
+ ],
828
+ },
829
+ ];
830
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
831
+ const results = await backend.queryMetrics({});
832
+ assert.strictEqual(results.length, 1);
833
+ assert.ok(results[0].exemplars);
834
+ assert.strictEqual(results[0].exemplars?.length, 1);
835
+ assert.strictEqual(results[0].exemplars?.[0].value, 1);
836
+ // Timestamp should default to metric timestamp
837
+ assert.strictEqual(results[0].exemplars?.[0].timestamp, '2026-01-28T10:00:00Z');
838
+ assert.strictEqual(results[0].exemplars?.[0].traceId, undefined);
839
+ assert.strictEqual(results[0].exemplars?.[0].spanId, undefined);
840
+ });
841
+ it('should handle metrics without exemplars', async () => {
842
+ const today = getTestDate();
843
+ const mockMetrics = [
844
+ {
845
+ timestamp: '2026-01-28T10:00:00Z',
846
+ name: 'simple.counter',
847
+ value: 42,
848
+ type: 'counter',
849
+ // No exemplars field
850
+ },
851
+ ];
852
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
853
+ const results = await backend.queryMetrics({});
854
+ assert.strictEqual(results.length, 1);
855
+ assert.strictEqual(results[0].exemplars, undefined);
856
+ });
857
+ it('should handle empty exemplars array', async () => {
858
+ const today = getTestDate();
859
+ const mockMetrics = [
860
+ {
861
+ timestamp: '2026-01-28T10:00:00Z',
862
+ name: 'empty.exemplars',
863
+ value: 100,
864
+ type: 'counter',
865
+ exemplars: [], // Empty array
866
+ },
867
+ ];
868
+ await writeJsonlFileAsync(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
869
+ const results = await backend.queryMetrics({});
870
+ assert.strictEqual(results.length, 1);
871
+ // Empty exemplars array should result in undefined
872
+ assert.strictEqual(results[0].exemplars, undefined);
873
+ });
874
+ });
875
+ });
876
+ //# sourceMappingURL=local-jsonl-metrics.test.js.map