apm-optima 1.0.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +54 -23
  2. package/dashboard-out/404/index.html +1 -1
  3. package/dashboard-out/404.html +1 -1
  4. package/dashboard-out/__next.__PAGE__.txt +4 -4
  5. package/dashboard-out/__next._full.txt +17 -17
  6. package/dashboard-out/__next._head.txt +4 -4
  7. package/dashboard-out/__next._index.txt +10 -10
  8. package/dashboard-out/__next._tree.txt +2 -2
  9. package/dashboard-out/_next/static/chunks/{2xjcsu4pgp14_.js → 062oipeclh9bq.js} +1 -1
  10. package/dashboard-out/_next/static/chunks/0ncbimuip0-o7.css +3 -0
  11. package/dashboard-out/_next/static/chunks/0xtgenoch3q77.js +1 -0
  12. package/dashboard-out/_next/static/chunks/1xln1-s87x5yp.js +27 -0
  13. package/dashboard-out/_not-found/__next._full.txt +15 -15
  14. package/dashboard-out/_not-found/__next._head.txt +4 -4
  15. package/dashboard-out/_not-found/__next._index.txt +10 -10
  16. package/dashboard-out/_not-found/__next._not-found.__PAGE__.txt +2 -2
  17. package/dashboard-out/_not-found/__next._not-found.txt +3 -3
  18. package/dashboard-out/_not-found/__next._tree.txt +2 -2
  19. package/dashboard-out/_not-found/index.html +1 -1
  20. package/dashboard-out/_not-found/index.txt +15 -15
  21. package/dashboard-out/index.html +1 -1
  22. package/dashboard-out/index.txt +17 -17
  23. package/dist/adapters/session-export.handler.d.ts +4 -0
  24. package/dist/adapters/session-export.handler.js +29 -0
  25. package/dist/adapters/socket.options.d.ts +3 -0
  26. package/dist/adapters/socket.options.js +16 -0
  27. package/dist/config/config.manager.d.ts +80 -3
  28. package/dist/config/config.manager.js +269 -89
  29. package/dist/config/config.types.d.ts +89 -13
  30. package/dist/core/delivery/buckets-request.d.ts +2 -0
  31. package/dist/core/delivery/buckets-request.js +18 -0
  32. package/dist/core/delivery/index.d.ts +1 -0
  33. package/dist/core/delivery/index.js +1 -0
  34. package/dist/core/delivery/metrics.publisher.d.ts +23 -10
  35. package/dist/core/delivery/metrics.publisher.js +22 -22
  36. package/dist/core/delivery/websocket.events.d.ts +14 -3
  37. package/dist/core/delivery/websocket.events.js +13 -2
  38. package/dist/core/domain/correlation.types.d.ts +37 -0
  39. package/dist/core/domain/correlation.types.js +2 -0
  40. package/dist/core/domain/dashboard.types.d.ts +16 -69
  41. package/dist/core/domain/dashboard.types.js +29 -0
  42. package/dist/core/domain/endpoints.types.d.ts +3 -0
  43. package/dist/core/domain/event.types.d.ts +14 -0
  44. package/dist/core/domain/event.types.js +2 -0
  45. package/dist/core/domain/incident.types.d.ts +50 -0
  46. package/dist/core/domain/incident.types.js +2 -0
  47. package/dist/core/domain/index.d.ts +5 -0
  48. package/dist/core/domain/index.js +5 -0
  49. package/dist/core/domain/live-bucket.types.d.ts +86 -0
  50. package/dist/core/domain/live-bucket.types.js +2 -0
  51. package/dist/core/domain/metrics.types.d.ts +41 -11
  52. package/dist/core/domain/session.types.d.ts +93 -0
  53. package/dist/core/domain/session.types.js +2 -0
  54. package/dist/core/domain/telemetry.types.d.ts +1 -0
  55. package/dist/core/organizers/event.manager.d.ts +19 -0
  56. package/dist/core/organizers/event.manager.js +73 -0
  57. package/dist/core/organizers/index.d.ts +2 -0
  58. package/dist/core/organizers/index.js +18 -0
  59. package/dist/core/organizers/interval.manager.d.ts +18 -0
  60. package/dist/core/organizers/interval.manager.js +54 -0
  61. package/dist/core/storage/index.d.ts +1 -0
  62. package/dist/core/storage/index.js +1 -0
  63. package/dist/core/storage/local.repository.d.ts +12 -12
  64. package/dist/core/storage/local.repository.js +38 -38
  65. package/dist/core/storage/persistence/persistence-writer.d.ts +48 -0
  66. package/dist/core/storage/persistence/persistence-writer.js +191 -0
  67. package/dist/core/storage/persistence/segment-index.d.ts +27 -0
  68. package/dist/core/storage/persistence/segment-index.js +206 -0
  69. package/dist/core/storage/persistence/session-registry.d.ts +28 -0
  70. package/dist/core/storage/persistence/session-registry.js +127 -0
  71. package/dist/core/storage/persistence/session-summary.d.ts +3 -0
  72. package/dist/core/storage/persistence/session-summary.js +112 -0
  73. package/dist/core/storage/persistence.layer.d.ts +69 -0
  74. package/dist/core/storage/persistence.layer.js +404 -0
  75. package/dist/core/storage/runtime/anomaly-detector.d.ts +11 -0
  76. package/dist/core/storage/runtime/anomaly-detector.js +42 -0
  77. package/dist/core/storage/stores/analytics.store.d.ts +5 -5
  78. package/dist/core/storage/stores/analytics.store.js +39 -12
  79. package/dist/core/storage/stores/bucket-metric.d.ts +85 -0
  80. package/dist/core/storage/stores/bucket-metric.js +168 -0
  81. package/dist/core/storage/stores/bucket-view.d.ts +44 -0
  82. package/dist/core/storage/stores/bucket-view.js +63 -0
  83. package/dist/core/storage/stores/bucket.store.d.ts +39 -36
  84. package/dist/core/storage/stores/bucket.store.js +112 -60
  85. package/dist/core/storage/stores/dashboard.store.d.ts +11 -4
  86. package/dist/core/storage/stores/dashboard.store.js +179 -47
  87. package/dist/core/storage/stores/endpoint.store.d.ts +1 -0
  88. package/dist/core/storage/stores/endpoint.store.js +21 -5
  89. package/dist/core/storage/stores/index.d.ts +0 -7
  90. package/dist/core/storage/stores/index.js +0 -7
  91. package/dist/core/storage/stores/metrics.store.d.ts +1 -1
  92. package/dist/core/storage/stores/metrics.store.js +1 -0
  93. package/dist/core/storage/stores/runtime.store.d.ts +77 -0
  94. package/dist/core/storage/stores/runtime.store.js +222 -0
  95. package/dist/core/storage/utility/common.d.ts +1 -0
  96. package/dist/core/storage/utility/common.js +11 -0
  97. package/dist/core/storage/utility/file-buffer.d.ts +31 -0
  98. package/dist/core/storage/utility/file-buffer.js +238 -0
  99. package/dist/core/storage/utility/histogram.d.ts +1 -2
  100. package/dist/core/storage/utility/histogram.js +1 -2
  101. package/dist/core/storage/utility/index.d.ts +2 -0
  102. package/dist/core/storage/utility/index.js +2 -0
  103. package/dist/core/storage/utility/persistence-files.d.ts +3 -0
  104. package/dist/core/storage/utility/persistence-files.js +34 -0
  105. package/dist/core/storage/utility/persistence-writer.d.ts +39 -0
  106. package/dist/core/storage/utility/persistence-writer.js +183 -0
  107. package/dist/core/storage/utility/session-export.d.ts +3 -0
  108. package/dist/core/storage/utility/session-export.js +31 -0
  109. package/dist/core/storage/utility/session-registry.d.ts +21 -0
  110. package/dist/core/storage/utility/session-registry.js +91 -0
  111. package/dist/core/storage/utility/session-window.d.ts +8 -0
  112. package/dist/core/storage/utility/session-window.js +43 -0
  113. package/dist/core/telemetry/analytics.service.d.ts +18 -0
  114. package/dist/core/telemetry/analytics.service.js +100 -0
  115. package/dist/core/telemetry/collector.service.d.ts +17 -15
  116. package/dist/core/telemetry/collector.service.js +23 -80
  117. package/dist/core/telemetry/correlation.service.d.ts +57 -9
  118. package/dist/core/telemetry/correlation.service.js +246 -54
  119. package/dist/core/telemetry/dashboard-live.service.d.ts +10 -0
  120. package/dist/core/telemetry/dashboard-live.service.js +110 -0
  121. package/dist/core/telemetry/dashboard.service.d.ts +20 -0
  122. package/dist/core/telemetry/dashboard.service.js +140 -0
  123. package/dist/core/telemetry/incident.service.d.ts +71 -0
  124. package/dist/core/telemetry/incident.service.js +239 -0
  125. package/dist/core/telemetry/logger.d.ts +4 -0
  126. package/dist/core/telemetry/logger.js +5 -2
  127. package/dist/core/telemetry/runtime.service.d.ts +14 -0
  128. package/dist/core/telemetry/runtime.service.js +56 -0
  129. package/dist/core/telemetry/telemetry-query.service.d.ts +82 -0
  130. package/dist/core/telemetry/telemetry-query.service.js +231 -0
  131. package/dist/core/telemetry/telemetry.service.d.ts +10 -4
  132. package/dist/core/telemetry/telemetry.service.js +60 -10
  133. package/dist/core/telemetry/utility/correlation-analysis.d.ts +9 -0
  134. package/dist/core/telemetry/utility/correlation-analysis.js +143 -0
  135. package/dist/core/telemetry/utility/correlation-finding.d.ts +70 -0
  136. package/dist/core/telemetry/utility/correlation-finding.js +41 -0
  137. package/dist/core/telemetry/utility/correlation-input.d.ts +59 -0
  138. package/dist/core/telemetry/utility/correlation-input.js +139 -0
  139. package/dist/core/telemetry/utility/correlation-replay.d.ts +24 -0
  140. package/dist/core/telemetry/utility/correlation-replay.js +46 -0
  141. package/dist/core/utility/correlation.analysis.d.ts +9 -0
  142. package/dist/core/utility/correlation.analysis.js +144 -0
  143. package/dist/core/utility/statistics.d.ts +65 -0
  144. package/dist/core/utility/statistics.js +141 -0
  145. package/dist/express/index.d.ts +1 -1
  146. package/dist/express/index.js +7 -5
  147. package/dist/express/metrics-websocket.adapter.d.ts +8 -2
  148. package/dist/express/metrics-websocket.adapter.js +52 -17
  149. package/dist/express/metrics.adapter.d.ts +1 -0
  150. package/dist/express/metrics.adapter.js +8 -1
  151. package/dist/express/metrics.bootstrap.d.ts +6 -1
  152. package/dist/express/metrics.bootstrap.js +48 -22
  153. package/dist/express/metrics.dashboard.d.ts +2 -1
  154. package/dist/express/metrics.dashboard.js +4 -1
  155. package/dist/express/metrics.middleware.d.ts +2 -1
  156. package/dist/express/metrics.middleware.js +23 -20
  157. package/dist/nest/metrics-websocket.adapter.d.ts +19 -3
  158. package/dist/nest/metrics-websocket.adapter.js +101 -21
  159. package/dist/nest/metrics.adapter.d.ts +4 -1
  160. package/dist/nest/metrics.adapter.js +24 -1
  161. package/dist/nest/metrics.bootstrap.service.d.ts +7 -3
  162. package/dist/nest/metrics.bootstrap.service.js +56 -21
  163. package/dist/nest/metrics.interceptor.d.ts +3 -3
  164. package/dist/nest/metrics.interceptor.js +11 -8
  165. package/dist/nest/metrics.module.d.ts +3 -0
  166. package/dist/nest/metrics.module.js +27 -9
  167. package/dist/runtime/index.d.ts +32 -0
  168. package/dist/runtime/index.js +104 -0
  169. package/dist/simulation/simulation.d.ts +5 -16
  170. package/dist/simulation/simulation.js +5 -8
  171. package/package.json +2 -1
  172. package/dashboard-out/_next/static/chunks/20t9qbodfrw_z.js +0 -3
  173. package/dashboard-out/_next/static/chunks/23soo6qkun-yv.js +0 -1
  174. package/dashboard-out/_next/static/chunks/3xoopocxi2vi8.css +0 -3
  175. /package/dashboard-out/_next/static/{u8vU2V6fhASeGVCpWU3eW → 17egwqgk36_DSRF55LJZG}/_buildManifest.js +0 -0
  176. /package/dashboard-out/_next/static/{u8vU2V6fhASeGVCpWU3eW → 17egwqgk36_DSRF55LJZG}/_clientMiddlewareManifest.js +0 -0
  177. /package/dashboard-out/_next/static/{u8vU2V6fhASeGVCpWU3eW → 17egwqgk36_DSRF55LJZG}/_ssgManifest.js +0 -0
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IncidentService = exports.CAUSE_NOTE = exports.initialTracker = void 0;
4
+ exports.stepRule = stepRule;
5
+ exports.defaultIncidentRules = defaultIncidentRules;
6
+ const bucket_metric_1 = require("../storage/stores/bucket-metric");
7
+ const initialTracker = () => ({ state: 'NORMAL', sinceMs: null, breachStartMs: null, recoveryStartMs: null });
8
+ exports.initialTracker = initialTracker;
9
+ function stepRule(t, obs, rule, timing) {
10
+ const now = obs.endMs;
11
+ const valid = obs.valid && obs.value !== null;
12
+ const breach = valid && obs.value > rule.threshold;
13
+ switch (t.state) {
14
+ case 'NORMAL':
15
+ return breach ? { state: 'PENDING', sinceMs: now, breachStartMs: now, recoveryStartMs: null } : t;
16
+ case 'PENDING':
17
+ if (!breach)
18
+ return { state: 'NORMAL', sinceMs: now, breachStartMs: null, recoveryStartMs: null };
19
+ if (now - t.breachStartMs >= timing.pendingForMs) {
20
+ return { state: 'FIRING', sinceMs: now, breachStartMs: t.breachStartMs, recoveryStartMs: null };
21
+ }
22
+ return t;
23
+ case 'FIRING': {
24
+ if (!valid || obs.value > rule.recoverBelow)
25
+ return { ...t, recoveryStartMs: null };
26
+ const recoveryStartMs = t.recoveryStartMs ?? now;
27
+ if (now - recoveryStartMs >= timing.recoveryForMs) {
28
+ return { state: 'RESOLVED', sinceMs: now, breachStartMs: null, recoveryStartMs: null };
29
+ }
30
+ return { ...t, recoveryStartMs };
31
+ }
32
+ case 'RESOLVED':
33
+ if (breach)
34
+ return { state: 'PENDING', sinceMs: now, breachStartMs: now, recoveryStartMs: null };
35
+ if (now - (t.sinceMs ?? now) >= timing.resolvedHoldMs) {
36
+ return { state: 'NORMAL', sinceMs: now, breachStartMs: null, recoveryStartMs: null };
37
+ }
38
+ return t;
39
+ }
40
+ }
41
+ function completeWindow(w, windowIntervals) {
42
+ if (w.total.bucketCount < windowIntervals)
43
+ return 'INCOMPLETE_WINDOW';
44
+ if (w.issues.some(i => i.type === 'GAP'))
45
+ return 'GAPS';
46
+ return undefined;
47
+ }
48
+ function defaultIncidentRules(o) {
49
+ const requestWindow = (w) => {
50
+ const incomplete = completeWindow(w, o.windowIntervals);
51
+ if (incomplete)
52
+ return incomplete;
53
+ if (w.total.requests.requestCount < o.minRequests)
54
+ return 'TOO_FEW_REQUESTS';
55
+ return undefined;
56
+ };
57
+ return [
58
+ {
59
+ id: 'server-error-rate',
60
+ title: 'High server error rate (5xx)',
61
+ metric: 'errorRate',
62
+ unit: '%',
63
+ severity: 'critical',
64
+ threshold: o.serverErrorRate * 100,
65
+ recoverBelow: o.serverErrorRate * 100 * o.recoveryRatio,
66
+ measure: w => {
67
+ const reason = requestWindow(w);
68
+ if (reason)
69
+ return { valid: false, value: null, reason };
70
+ const r = w.total.requests;
71
+ return { valid: true, value: (r.serverErrorCount / r.requestCount) * 100 };
72
+ },
73
+ },
74
+ {
75
+ id: 'p95-latency',
76
+ title: 'High p95 latency',
77
+ metric: 'p95Latency',
78
+ unit: 'ms',
79
+ severity: 'warning',
80
+ threshold: o.p95LatencyMs,
81
+ recoverBelow: o.p95LatencyMs * o.recoveryRatio,
82
+ measure: w => {
83
+ const reason = requestWindow(w);
84
+ if (reason)
85
+ return { valid: false, value: null, reason };
86
+ return { valid: true, value: (0, bucket_metric_1.deriveMetrics)(w.total.requests, w.total.durationMs).p95 };
87
+ },
88
+ },
89
+ {
90
+ id: 'event-loop-lag',
91
+ title: 'High event loop lag',
92
+ metric: 'eventLoopLag',
93
+ unit: 'ms',
94
+ severity: 'warning',
95
+ threshold: o.eventLoopLagMs,
96
+ recoverBelow: o.eventLoopLagMs * o.recoveryRatio,
97
+ measure: w => {
98
+ const incomplete = completeWindow(w, o.windowIntervals);
99
+ if (incomplete)
100
+ return { valid: false, value: null, reason: incomplete };
101
+ const runtime = (0, bucket_metric_1.emptyRuntime)();
102
+ for (const p of w.points)
103
+ (0, bucket_metric_1.mergeRuntimeInto)(runtime, p.runtime, true);
104
+ const mean = (0, bucket_metric_1.deriveRuntime)(runtime, w.total.durationMs).eventLoopDelayMeanMs;
105
+ return mean === null
106
+ ? { valid: false, value: null, reason: 'NO_EVENT_LOOP_SAMPLES' }
107
+ : { valid: true, value: mean };
108
+ },
109
+ },
110
+ ];
111
+ }
112
+ exports.CAUSE_NOTE = 'Related findings show which metrics changed together during the incident. Correlation does not establish the cause.';
113
+ class IncidentService {
114
+ deps;
115
+ timing;
116
+ windowIntervals;
117
+ historySize;
118
+ entries;
119
+ history = [];
120
+ lastSequence = null;
121
+ lastSessionId = null;
122
+ constructor(deps, options) {
123
+ this.deps = deps;
124
+ this.timing = {
125
+ pendingForMs: options.pendingForMs,
126
+ recoveryForMs: options.recoveryForMs,
127
+ resolvedHoldMs: options.resolvedHoldMs,
128
+ };
129
+ this.windowIntervals = options.windowIntervals;
130
+ this.historySize = options.historySize;
131
+ const rules = options.rules ?? defaultIncidentRules(options);
132
+ this.entries = rules.map(rule => ({ rule, tracker: (0, exports.initialTracker)(), last: null, incident: null }));
133
+ }
134
+ tick() {
135
+ this.evaluate();
136
+ }
137
+ evaluate() {
138
+ const sessionId = this.deps.sessionId();
139
+ if (sessionId !== this.lastSessionId) {
140
+ this.lastSessionId = sessionId;
141
+ this.lastSequence = null;
142
+ for (const e of this.entries) {
143
+ e.tracker = (0, exports.initialTracker)();
144
+ e.last = null;
145
+ e.incident = null;
146
+ }
147
+ }
148
+ const window = this.deps.queries.recent(this.windowIntervals, {
149
+ consistency: this.deps.durable() ? 'committed' : 'live',
150
+ });
151
+ const last = window.points[window.points.length - 1];
152
+ if (!last || last.sequenceTo === this.lastSequence)
153
+ return false;
154
+ this.lastSequence = last.sequenceTo;
155
+ const endMs = Date.parse(last.endTime);
156
+ for (const entry of this.entries) {
157
+ const obs = { ...entry.rule.measure(window), endMs };
158
+ const prev = entry.tracker;
159
+ entry.tracker = stepRule(prev, obs, entry.rule, this.timing);
160
+ entry.last = obs;
161
+ this.apply(entry, prev.state, window, sessionId);
162
+ }
163
+ return true;
164
+ }
165
+ snapshot() {
166
+ const iso = (ms) => ms === null ? null : new Date(ms).toISOString();
167
+ const rules = this.entries.map(({ rule, tracker, last, incident }) => ({
168
+ ruleId: rule.id,
169
+ title: rule.title,
170
+ metric: rule.metric,
171
+ unit: rule.unit,
172
+ severity: rule.severity,
173
+ state: tracker.state,
174
+ since: iso(tracker.sinceMs),
175
+ value: last?.value ?? null,
176
+ threshold: rule.threshold,
177
+ recoverBelow: rule.recoverBelow,
178
+ valid: last?.valid ?? false,
179
+ reason: last?.reason,
180
+ incidentId: incident?.incidentId ?? null,
181
+ }));
182
+ const incidents = [...this.history].sort((a, b) => (a.status === b.status ? 0 : a.status === 'FIRING' ? -1 : 1) || b.firedAt.localeCompare(a.firedAt));
183
+ return { rules, incidents, serverTime: new Date().toISOString() };
184
+ }
185
+ apply(entry, prevState, window, sessionId) {
186
+ const { rule, tracker, last } = entry;
187
+ const value = last?.valid ? last.value : null;
188
+ if (prevState !== 'FIRING' && tracker.state === 'FIRING') {
189
+ const first = window.points[0];
190
+ const lastPoint = window.points[window.points.length - 1];
191
+ const incident = {
192
+ incidentId: `${sessionId}:${rule.id}:${lastPoint.sequenceTo}`,
193
+ sessionId,
194
+ ruleId: rule.id,
195
+ title: rule.title,
196
+ metric: rule.metric,
197
+ unit: rule.unit,
198
+ severity: rule.severity,
199
+ status: 'FIRING',
200
+ threshold: rule.threshold,
201
+ recoverBelow: rule.recoverBelow,
202
+ breachStartedAt: new Date(tracker.breachStartMs).toISOString(),
203
+ firedAt: new Date(tracker.sinceMs).toISOString(),
204
+ resolvedAt: null,
205
+ peakValue: value ?? rule.threshold,
206
+ lastValue: value,
207
+ sequenceFrom: first.sequenceFrom,
208
+ sequenceTo: lastPoint.sequenceTo,
209
+ relatedFindings: this.deps.related?.(rule.metric) ?? [],
210
+ causeNote: exports.CAUSE_NOTE,
211
+ };
212
+ entry.incident = incident;
213
+ this.history.push(incident);
214
+ if (this.history.length > this.historySize)
215
+ this.history.splice(0, this.history.length - this.historySize);
216
+ void this.deps.emit?.({ type: 'INCIDENT_OPENED', reason: `${rule.title}`, details: { ...incident } });
217
+ return;
218
+ }
219
+ const incident = entry.incident;
220
+ if (!incident)
221
+ return;
222
+ if (tracker.state === 'FIRING') {
223
+ if (value !== null) {
224
+ incident.lastValue = value;
225
+ incident.peakValue = Math.max(incident.peakValue, value);
226
+ }
227
+ return;
228
+ }
229
+ if (prevState === 'FIRING' && tracker.state === 'RESOLVED') {
230
+ incident.status = 'RESOLVED';
231
+ incident.resolvedAt = new Date(tracker.sinceMs).toISOString();
232
+ if (value !== null)
233
+ incident.lastValue = value;
234
+ entry.incident = null;
235
+ void this.deps.emit?.({ type: 'INCIDENT_RESOLVED', reason: `${rule.title}`, details: { ...incident } });
236
+ }
237
+ }
238
+ }
239
+ exports.IncidentService = IncidentService;
@@ -3,6 +3,10 @@ declare class Logger {
3
3
  private static readonly RESET;
4
4
  private static readonly BOLD;
5
5
  private static readonly DIM;
6
+ private static consoleLog;
7
+ static configure(options: {
8
+ consoleLog: boolean;
9
+ }): void;
6
10
  private static readonly COLORS;
7
11
  static log(req: Request, res: Response, duration: number): void;
8
12
  static error(message: string, ...args: unknown[]): void;
@@ -1,10 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const config_1 = require("../../config");
4
3
  class Logger {
5
4
  static RESET = '\x1b[0m';
6
5
  static BOLD = '\x1b[1m';
7
6
  static DIM = '\x1b[2m';
7
+ static consoleLog = true;
8
+ static configure(options) {
9
+ Logger.consoleLog = options.consoleLog;
10
+ }
8
11
  static COLORS = {
9
12
  GET: '\x1b[32m', // Green
10
13
  POST: '\x1b[36m', // Cyan
@@ -16,7 +19,7 @@ class Logger {
16
19
  ERROR: '\x1b[31m' // Red (5xx)
17
20
  };
18
21
  static log(req, res, duration) {
19
- if (!(0, config_1.getConfig)().consoleLog)
22
+ if (!Logger.consoleLog)
20
23
  return;
21
24
  const timestamp = new Date().toISOString().split('T')[1].slice(0, -1);
22
25
  const timeFormatted = `${duration.toFixed(2)}ms`.padStart(8, ' ');
@@ -0,0 +1,14 @@
1
+ import type { ReadonlyConfig } from "../../config";
2
+ import type { HealthDetails, SystemStaticInfo } from "../domain";
3
+ import type { LocalRepository } from "../storage";
4
+ export declare class RuntimeService {
5
+ private readonly storage;
6
+ private readonly config;
7
+ readonly eventLoopLagThresholdMs: number;
8
+ readonly env: string;
9
+ readonly startedAt: number;
10
+ readonly nodeVersion: string;
11
+ constructor(storage: LocalRepository, config: ReadonlyConfig);
12
+ getSystemStaticInfo(): SystemStaticInfo;
13
+ get(): HealthDetails;
14
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RuntimeService = void 0;
4
+ class RuntimeService {
5
+ storage;
6
+ config;
7
+ eventLoopLagThresholdMs;
8
+ env;
9
+ startedAt;
10
+ nodeVersion;
11
+ constructor(storage, config) {
12
+ this.storage = storage;
13
+ this.config = config;
14
+ this.eventLoopLagThresholdMs = config.thresholds.eventLoopLagMs;
15
+ this.env = process.env.NODE_ENV ?? 'unknown';
16
+ this.nodeVersion = process.version;
17
+ this.startedAt = Date.now();
18
+ }
19
+ getSystemStaticInfo() {
20
+ return {
21
+ uptime: Date.now() - this.startedAt,
22
+ nodeVersion: this.nodeVersion,
23
+ env: this.env
24
+ };
25
+ }
26
+ get() {
27
+ const details = this.storage.runtime.details();
28
+ const gc = details.gc;
29
+ return {
30
+ cpu: {
31
+ numberOfCores: details.numberOfCores,
32
+ perCoreUsage: details.perCoreUsage,
33
+ },
34
+ memory: {
35
+ rssMemoryTotal: details.rssMemoryTotal,
36
+ externalMemory: details.externalMemory,
37
+ },
38
+ eventLoop: {
39
+ threshold: this.eventLoopLagThresholdMs,
40
+ },
41
+ handles: details.handles,
42
+ garbageCollection: {
43
+ gcCount: gc.gcCount,
44
+ gcTime: gc.gcTime,
45
+ gcPauseAverage: gc.gcCount > 0 ? parseFloat((gc.gcTime / gc.gcCount).toFixed(2)) : 0,
46
+ minorGC: gc.minorGC,
47
+ majorGC: gc.majorGC,
48
+ incrementalGC: gc.incrementalGC,
49
+ heapSpaces: gc.heapSpaces,
50
+ gcTotals: gc.gcTotals,
51
+ },
52
+ runtime: details.runtime,
53
+ };
54
+ }
55
+ }
56
+ exports.RuntimeService = RuntimeService;
@@ -0,0 +1,82 @@
1
+ import { deriveMetrics, type MetricBucket, type RequestAggregate, type EndpointAggregate, type RuntimeInterval } from "../storage/stores/bucket-metric";
2
+ export type MetricsQuery = {
3
+ sessionId: string;
4
+ from: string;
5
+ to: string;
6
+ method?: string;
7
+ route?: string;
8
+ resolutionMs?: number;
9
+ consistency: 'committed' | 'live';
10
+ includeEndpoints?: boolean;
11
+ };
12
+ export type QueryIssue = {
13
+ type: 'CONFLICT';
14
+ bucketId: string;
15
+ } | {
16
+ type: 'GAP';
17
+ fromSequence: number;
18
+ toSequence: number;
19
+ } | {
20
+ type: 'OUT_OF_ORDER';
21
+ bucketId: string;
22
+ } | {
23
+ type: 'SOURCE_ERROR';
24
+ detail: string;
25
+ };
26
+ export interface SeriesPoint {
27
+ startTime: string;
28
+ endTime: string;
29
+ durationMs: number;
30
+ bucketCount: number;
31
+ sequenceFrom: number;
32
+ sequenceTo: number;
33
+ requests: RequestAggregate;
34
+ maxRps: number;
35
+ endpoints?: EndpointAggregate[];
36
+ runtime: RuntimeInterval;
37
+ qualityFlags: string[];
38
+ committed: boolean | null;
39
+ }
40
+ export interface MetricsQueryResult {
41
+ query: MetricsQuery;
42
+ points: SeriesPoint[];
43
+ total: {
44
+ requests: RequestAggregate;
45
+ durationMs: number;
46
+ bucketCount: number;
47
+ maxRps: number;
48
+ derived: ReturnType<typeof deriveMetrics>;
49
+ };
50
+ issues: QueryIssue[];
51
+ complete: boolean;
52
+ }
53
+ export interface CommittedBucketSource {
54
+ read(sessionId: string, fromMs: number, toMs: number, onIssue: (issue: QueryIssue) => void): AsyncIterable<MetricBucket>;
55
+ }
56
+ export interface LiveBucketEntry {
57
+ bucket: MetricBucket;
58
+ committed: boolean;
59
+ }
60
+ export interface LiveBucketSource {
61
+ snapshot(): LiveBucketEntry[];
62
+ }
63
+ export interface TelemetryQueryDeps {
64
+ committed: CommittedBucketSource;
65
+ live: LiveBucketSource;
66
+ currentSessionId: () => string;
67
+ }
68
+ export declare class TelemetryQueryService {
69
+ private readonly deps;
70
+ constructor(deps: TelemetryQueryDeps);
71
+ query(q: MetricsQuery): Promise<MetricsQueryResult>;
72
+ recent(count: number, opts?: Partial<Pick<MetricsQuery, 'consistency' | 'method' | 'route' | 'resolutionMs' | 'includeEndpoints'>>): MetricsQueryResult;
73
+ latestWindow(count: number, consistency: MetricsQuery['consistency']): {
74
+ sessionId: string;
75
+ from: string;
76
+ to: string;
77
+ sequenceFrom: number;
78
+ sequenceTo: number;
79
+ } | null;
80
+ liveBucket(sequence: number): MetricBucket | undefined;
81
+ buckets(q: MetricsQuery, onIssue: (i: QueryIssue) => void): AsyncGenerator<LiveBucketEntry>;
82
+ }
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TelemetryQueryService = void 0;
4
+ const bucket_metric_1 = require("../storage/stores/bucket-metric");
5
+ function stableStringify(v) {
6
+ if (v === null || typeof v !== 'object')
7
+ return JSON.stringify(v);
8
+ if (Array.isArray(v))
9
+ return `[${v.map(stableStringify).join(',')}]`;
10
+ const o = v;
11
+ return `{${Object.keys(o).sort().map(k => `${JSON.stringify(k)}:${stableStringify(o[k])}`).join(',')}}`;
12
+ }
13
+ const fingerprint = (b) => stableStringify(b);
14
+ function parseRange(q) {
15
+ const fromMs = Date.parse(q.from);
16
+ const toMs = Date.parse(q.to);
17
+ if (Number.isNaN(fromMs) || Number.isNaN(toMs))
18
+ throw new RangeError('Invalid time range');
19
+ if (fromMs > toMs)
20
+ throw new RangeError('`from` must not be after `to`');
21
+ return { fromMs, toMs };
22
+ }
23
+ const overlaps = (b, fromMs, toMs) => Date.parse(b.startTime) <= toMs && Date.parse(b.endTime) >= fromMs;
24
+ function project(b, q) {
25
+ if (!q.method && !q.route)
26
+ return { requests: b.requests, endpoints: b.endpoints };
27
+ const matching = b.endpoints.filter(e => (!q.method || e.method === q.method) && (!q.route || e.route === q.route));
28
+ const requests = (0, bucket_metric_1.createAggregate)();
29
+ for (const e of matching)
30
+ (0, bucket_metric_1.mergeInto)(requests, e);
31
+ return { requests, endpoints: matching };
32
+ }
33
+ class SeriesAccumulator {
34
+ q;
35
+ issues;
36
+ groups = new Map();
37
+ total = (0, bucket_metric_1.createAggregate)();
38
+ totalDurationMs = 0;
39
+ maxRps = 0;
40
+ bucketCount = 0;
41
+ prevSequence = null;
42
+ constructor(q, issues) {
43
+ this.q = q;
44
+ this.issues = issues;
45
+ }
46
+ add(b, committed) {
47
+ if (this.prevSequence !== null) {
48
+ if (b.sequence <= this.prevSequence) {
49
+ this.issues.push({ type: 'OUT_OF_ORDER', bucketId: b.bucketId });
50
+ }
51
+ else if (b.sequence > this.prevSequence + 1) {
52
+ this.issues.push({ type: 'GAP', fromSequence: this.prevSequence + 1, toSequence: b.sequence - 1 });
53
+ }
54
+ }
55
+ this.prevSequence = this.prevSequence === null ? b.sequence : Math.max(this.prevSequence, b.sequence);
56
+ const p = project(b, this.q);
57
+ this.bucketCount++;
58
+ this.totalDurationMs += b.durationMs;
59
+ (0, bucket_metric_1.mergeInto)(this.total, p.requests);
60
+ const bucketRps = (0, bucket_metric_1.deriveMetrics)(p.requests, b.durationMs).rps;
61
+ this.maxRps = Math.max(this.maxRps, bucketRps);
62
+ const size = this.q.resolutionMs && this.q.resolutionMs > 0 ? this.q.resolutionMs : 0;
63
+ const key = size ? Math.floor(Date.parse(b.startTime) / size) : b.sequence;
64
+ let g = this.groups.get(key);
65
+ if (!g) {
66
+ g = {
67
+ startTime: b.startTime, endTime: b.endTime,
68
+ durationMs: 0, bucketCount: 0,
69
+ sequenceFrom: b.sequence, sequenceTo: b.sequence,
70
+ requests: (0, bucket_metric_1.createAggregate)(),
71
+ maxRps: 0,
72
+ endpoints: this.q.includeEndpoints ? new Map() : undefined,
73
+ runtime: (0, bucket_metric_1.emptyRuntime)(),
74
+ flags: new Set(),
75
+ committedCount: 0,
76
+ uncommittedCount: 0
77
+ };
78
+ this.groups.set(key, g);
79
+ }
80
+ (0, bucket_metric_1.mergeInto)(g.requests, p.requests);
81
+ g.maxRps = Math.max(g.maxRps, bucketRps);
82
+ g.durationMs += b.durationMs;
83
+ g.bucketCount++;
84
+ if (committed === true)
85
+ g.committedCount++;
86
+ else if (committed === false)
87
+ g.uncommittedCount++;
88
+ if (b.startTime < g.startTime)
89
+ g.startTime = b.startTime;
90
+ if (b.endTime > g.endTime)
91
+ g.endTime = b.endTime;
92
+ g.sequenceFrom = Math.min(g.sequenceFrom, b.sequence);
93
+ const isLatest = b.sequence >= g.sequenceTo;
94
+ if (isLatest)
95
+ g.sequenceTo = b.sequence;
96
+ (0, bucket_metric_1.mergeRuntimeInto)(g.runtime, b.runtime, isLatest);
97
+ for (const f of b.qualityFlags)
98
+ g.flags.add(f);
99
+ if (g.endpoints) {
100
+ for (const e of p.endpoints) {
101
+ const ek = `${e.method}:${e.route}`;
102
+ let t = g.endpoints.get(ek);
103
+ if (!t) {
104
+ t = { method: e.method, route: e.route, ...(0, bucket_metric_1.createAggregate)() };
105
+ g.endpoints.set(ek, t);
106
+ }
107
+ (0, bucket_metric_1.mergeInto)(t, e);
108
+ }
109
+ }
110
+ }
111
+ finish() {
112
+ const points = [...this.groups.values()]
113
+ .sort((a, b) => (a.startTime < b.startTime ? -1 : a.startTime > b.startTime ? 1 : 0))
114
+ .map(g => ({
115
+ startTime: g.startTime,
116
+ endTime: g.endTime,
117
+ durationMs: g.durationMs,
118
+ bucketCount: g.bucketCount,
119
+ sequenceFrom: g.sequenceFrom,
120
+ sequenceTo: g.sequenceTo,
121
+ requests: g.requests,
122
+ maxRps: g.maxRps,
123
+ endpoints: g.endpoints ? [...g.endpoints.values()] : undefined,
124
+ runtime: g.runtime,
125
+ qualityFlags: [...g.flags].sort(),
126
+ committed: g.uncommittedCount > 0 ? false : g.committedCount === g.bucketCount ? true : null
127
+ }));
128
+ return {
129
+ query: this.q,
130
+ points,
131
+ total: {
132
+ requests: this.total,
133
+ durationMs: this.totalDurationMs,
134
+ bucketCount: this.bucketCount,
135
+ maxRps: this.maxRps,
136
+ derived: (0, bucket_metric_1.deriveMetrics)(this.total, this.totalDurationMs),
137
+ },
138
+ issues: this.issues,
139
+ complete: this.issues.length === 0,
140
+ };
141
+ }
142
+ }
143
+ class TelemetryQueryService {
144
+ deps;
145
+ constructor(deps) {
146
+ this.deps = deps;
147
+ }
148
+ async query(q) {
149
+ const issues = [];
150
+ const acc = new SeriesAccumulator(q, issues);
151
+ for await (const e of this.buckets(q, i => issues.push(i)))
152
+ acc.add(e.bucket, e.committed);
153
+ return acc.finish();
154
+ }
155
+ recent(count, opts = {}) {
156
+ const consistency = opts.consistency ?? 'live';
157
+ const entries = this.deps.live.snapshot()
158
+ .filter(e => consistency === 'live' || e.committed)
159
+ .slice(-Math.max(0, count));
160
+ const first = entries[0]?.bucket;
161
+ const last = entries[entries.length - 1]?.bucket;
162
+ const now = new Date().toISOString();
163
+ const q = {
164
+ sessionId: this.deps.currentSessionId(),
165
+ from: first?.startTime ?? now,
166
+ to: last?.endTime ?? now,
167
+ consistency,
168
+ method: opts.method,
169
+ route: opts.route,
170
+ resolutionMs: opts.resolutionMs,
171
+ includeEndpoints: opts.includeEndpoints,
172
+ };
173
+ const acc = new SeriesAccumulator(q, []);
174
+ for (const e of entries)
175
+ acc.add(e.bucket, e.committed);
176
+ return acc.finish();
177
+ }
178
+ latestWindow(count, consistency) {
179
+ const entries = this.deps.live.snapshot()
180
+ .filter(e => consistency === 'live' || e.committed)
181
+ .slice(-Math.max(1, count));
182
+ if (entries.length === 0)
183
+ return null;
184
+ const first = entries[0].bucket;
185
+ const last = entries[entries.length - 1].bucket;
186
+ return {
187
+ sessionId: this.deps.currentSessionId(),
188
+ from: first.startTime,
189
+ to: last.endTime,
190
+ sequenceFrom: first.sequence,
191
+ sequenceTo: last.sequence,
192
+ };
193
+ }
194
+ liveBucket(sequence) {
195
+ return this.deps.live.snapshot().find(e => e.bucket.sequence === sequence)?.bucket;
196
+ }
197
+ async *buckets(q, onIssue) {
198
+ const { fromMs, toMs } = parseRange(q);
199
+ const isCurrent = q.sessionId === this.deps.currentSessionId();
200
+ const ramAll = isCurrent ? this.deps.live.snapshot() : [];
201
+ const ram = ramAll.filter(e => overlaps(e.bucket, fromMs, toMs) && (q.consistency === 'live' || e.committed));
202
+ const ramById = new Map(ram.map(e => [e.bucket.bucketId, e.bucket]));
203
+ const oldest = ramAll[0]?.bucket;
204
+ const covered = oldest !== undefined &&
205
+ (oldest.sequence === 1 || Date.parse(oldest.startTime) <= fromMs);
206
+ const handled = new Set();
207
+ if (!covered) {
208
+ try {
209
+ for await (const b of this.deps.committed.read(q.sessionId, fromMs, toMs, onIssue)) {
210
+ const mem = ramById.get(b.bucketId);
211
+ if (mem && fingerprint(mem) !== fingerprint(b)) {
212
+ handled.add(b.bucketId);
213
+ onIssue({ type: 'CONFLICT', bucketId: b.bucketId });
214
+ continue;
215
+ }
216
+ handled.add(b.bucketId);
217
+ yield { bucket: b, committed: true };
218
+ }
219
+ }
220
+ catch (err) {
221
+ onIssue({ type: 'SOURCE_ERROR', detail: err instanceof Error ? err.message : String(err) });
222
+ }
223
+ }
224
+ for (const e of ram) {
225
+ if (handled.has(e.bucket.bucketId))
226
+ continue;
227
+ yield e;
228
+ }
229
+ }
230
+ }
231
+ exports.TelemetryQueryService = TelemetryQueryService;
@@ -1,10 +1,16 @@
1
1
  import type { TelemetryRequest } from '../domain';
2
- declare class TelemetryService {
2
+ import type { ReadonlyConfig } from '../../config';
3
+ export declare class TelemetryService {
3
4
  private readonly storage;
4
- constructor(storage?: {
5
+ private readonly excludePatterns;
6
+ private inFlight;
7
+ private drainWaiters;
8
+ constructor(storage: {
5
9
  record(request: TelemetryRequest): void;
6
- });
10
+ }, config: ReadonlyConfig);
7
11
  private isPathExcluded;
12
+ track(): () => void;
13
+ get inFlightCount(): number;
14
+ whenDrained(timeoutMs: number): Promise<boolean>;
8
15
  record(data: Omit<TelemetryRequest, 'timestamp' | 'responseTime'>, startHrTime: bigint): TelemetryRequest | null;
9
16
  }
10
- export default TelemetryService;