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.
- package/README.md +54 -23
- package/dashboard-out/404/index.html +1 -1
- package/dashboard-out/404.html +1 -1
- package/dashboard-out/__next.__PAGE__.txt +4 -4
- package/dashboard-out/__next._full.txt +17 -17
- package/dashboard-out/__next._head.txt +4 -4
- package/dashboard-out/__next._index.txt +10 -10
- package/dashboard-out/__next._tree.txt +2 -2
- package/dashboard-out/_next/static/chunks/{2xjcsu4pgp14_.js → 062oipeclh9bq.js} +1 -1
- package/dashboard-out/_next/static/chunks/0ncbimuip0-o7.css +3 -0
- package/dashboard-out/_next/static/chunks/0xtgenoch3q77.js +1 -0
- package/dashboard-out/_next/static/chunks/1xln1-s87x5yp.js +27 -0
- package/dashboard-out/_not-found/__next._full.txt +15 -15
- package/dashboard-out/_not-found/__next._head.txt +4 -4
- package/dashboard-out/_not-found/__next._index.txt +10 -10
- package/dashboard-out/_not-found/__next._not-found.__PAGE__.txt +2 -2
- package/dashboard-out/_not-found/__next._not-found.txt +3 -3
- package/dashboard-out/_not-found/__next._tree.txt +2 -2
- package/dashboard-out/_not-found/index.html +1 -1
- package/dashboard-out/_not-found/index.txt +15 -15
- package/dashboard-out/index.html +1 -1
- package/dashboard-out/index.txt +17 -17
- package/dist/adapters/session-export.handler.d.ts +4 -0
- package/dist/adapters/session-export.handler.js +29 -0
- package/dist/adapters/socket.options.d.ts +3 -0
- package/dist/adapters/socket.options.js +16 -0
- package/dist/config/config.manager.d.ts +80 -3
- package/dist/config/config.manager.js +269 -89
- package/dist/config/config.types.d.ts +89 -13
- package/dist/core/delivery/buckets-request.d.ts +2 -0
- package/dist/core/delivery/buckets-request.js +18 -0
- package/dist/core/delivery/index.d.ts +1 -0
- package/dist/core/delivery/index.js +1 -0
- package/dist/core/delivery/metrics.publisher.d.ts +23 -10
- package/dist/core/delivery/metrics.publisher.js +22 -22
- package/dist/core/delivery/websocket.events.d.ts +14 -3
- package/dist/core/delivery/websocket.events.js +13 -2
- package/dist/core/domain/correlation.types.d.ts +37 -0
- package/dist/core/domain/correlation.types.js +2 -0
- package/dist/core/domain/dashboard.types.d.ts +16 -69
- package/dist/core/domain/dashboard.types.js +29 -0
- package/dist/core/domain/endpoints.types.d.ts +3 -0
- package/dist/core/domain/event.types.d.ts +14 -0
- package/dist/core/domain/event.types.js +2 -0
- package/dist/core/domain/incident.types.d.ts +50 -0
- package/dist/core/domain/incident.types.js +2 -0
- package/dist/core/domain/index.d.ts +5 -0
- package/dist/core/domain/index.js +5 -0
- package/dist/core/domain/live-bucket.types.d.ts +86 -0
- package/dist/core/domain/live-bucket.types.js +2 -0
- package/dist/core/domain/metrics.types.d.ts +41 -11
- package/dist/core/domain/session.types.d.ts +93 -0
- package/dist/core/domain/session.types.js +2 -0
- package/dist/core/domain/telemetry.types.d.ts +1 -0
- package/dist/core/organizers/event.manager.d.ts +19 -0
- package/dist/core/organizers/event.manager.js +73 -0
- package/dist/core/organizers/index.d.ts +2 -0
- package/dist/core/organizers/index.js +18 -0
- package/dist/core/organizers/interval.manager.d.ts +18 -0
- package/dist/core/organizers/interval.manager.js +54 -0
- package/dist/core/storage/index.d.ts +1 -0
- package/dist/core/storage/index.js +1 -0
- package/dist/core/storage/local.repository.d.ts +12 -12
- package/dist/core/storage/local.repository.js +38 -38
- package/dist/core/storage/persistence/persistence-writer.d.ts +48 -0
- package/dist/core/storage/persistence/persistence-writer.js +191 -0
- package/dist/core/storage/persistence/segment-index.d.ts +27 -0
- package/dist/core/storage/persistence/segment-index.js +206 -0
- package/dist/core/storage/persistence/session-registry.d.ts +28 -0
- package/dist/core/storage/persistence/session-registry.js +127 -0
- package/dist/core/storage/persistence/session-summary.d.ts +3 -0
- package/dist/core/storage/persistence/session-summary.js +112 -0
- package/dist/core/storage/persistence.layer.d.ts +69 -0
- package/dist/core/storage/persistence.layer.js +404 -0
- package/dist/core/storage/runtime/anomaly-detector.d.ts +11 -0
- package/dist/core/storage/runtime/anomaly-detector.js +42 -0
- package/dist/core/storage/stores/analytics.store.d.ts +5 -5
- package/dist/core/storage/stores/analytics.store.js +39 -12
- package/dist/core/storage/stores/bucket-metric.d.ts +85 -0
- package/dist/core/storage/stores/bucket-metric.js +168 -0
- package/dist/core/storage/stores/bucket-view.d.ts +44 -0
- package/dist/core/storage/stores/bucket-view.js +63 -0
- package/dist/core/storage/stores/bucket.store.d.ts +39 -36
- package/dist/core/storage/stores/bucket.store.js +112 -60
- package/dist/core/storage/stores/dashboard.store.d.ts +11 -4
- package/dist/core/storage/stores/dashboard.store.js +179 -47
- package/dist/core/storage/stores/endpoint.store.d.ts +1 -0
- package/dist/core/storage/stores/endpoint.store.js +21 -5
- package/dist/core/storage/stores/index.d.ts +0 -7
- package/dist/core/storage/stores/index.js +0 -7
- package/dist/core/storage/stores/metrics.store.d.ts +1 -1
- package/dist/core/storage/stores/metrics.store.js +1 -0
- package/dist/core/storage/stores/runtime.store.d.ts +77 -0
- package/dist/core/storage/stores/runtime.store.js +222 -0
- package/dist/core/storage/utility/common.d.ts +1 -0
- package/dist/core/storage/utility/common.js +11 -0
- package/dist/core/storage/utility/file-buffer.d.ts +31 -0
- package/dist/core/storage/utility/file-buffer.js +238 -0
- package/dist/core/storage/utility/histogram.d.ts +1 -2
- package/dist/core/storage/utility/histogram.js +1 -2
- package/dist/core/storage/utility/index.d.ts +2 -0
- package/dist/core/storage/utility/index.js +2 -0
- package/dist/core/storage/utility/persistence-files.d.ts +3 -0
- package/dist/core/storage/utility/persistence-files.js +34 -0
- package/dist/core/storage/utility/persistence-writer.d.ts +39 -0
- package/dist/core/storage/utility/persistence-writer.js +183 -0
- package/dist/core/storage/utility/session-export.d.ts +3 -0
- package/dist/core/storage/utility/session-export.js +31 -0
- package/dist/core/storage/utility/session-registry.d.ts +21 -0
- package/dist/core/storage/utility/session-registry.js +91 -0
- package/dist/core/storage/utility/session-window.d.ts +8 -0
- package/dist/core/storage/utility/session-window.js +43 -0
- package/dist/core/telemetry/analytics.service.d.ts +18 -0
- package/dist/core/telemetry/analytics.service.js +100 -0
- package/dist/core/telemetry/collector.service.d.ts +17 -15
- package/dist/core/telemetry/collector.service.js +23 -80
- package/dist/core/telemetry/correlation.service.d.ts +57 -9
- package/dist/core/telemetry/correlation.service.js +246 -54
- package/dist/core/telemetry/dashboard-live.service.d.ts +10 -0
- package/dist/core/telemetry/dashboard-live.service.js +110 -0
- package/dist/core/telemetry/dashboard.service.d.ts +20 -0
- package/dist/core/telemetry/dashboard.service.js +140 -0
- package/dist/core/telemetry/incident.service.d.ts +71 -0
- package/dist/core/telemetry/incident.service.js +239 -0
- package/dist/core/telemetry/logger.d.ts +4 -0
- package/dist/core/telemetry/logger.js +5 -2
- package/dist/core/telemetry/runtime.service.d.ts +14 -0
- package/dist/core/telemetry/runtime.service.js +56 -0
- package/dist/core/telemetry/telemetry-query.service.d.ts +82 -0
- package/dist/core/telemetry/telemetry-query.service.js +231 -0
- package/dist/core/telemetry/telemetry.service.d.ts +10 -4
- package/dist/core/telemetry/telemetry.service.js +60 -10
- package/dist/core/telemetry/utility/correlation-analysis.d.ts +9 -0
- package/dist/core/telemetry/utility/correlation-analysis.js +143 -0
- package/dist/core/telemetry/utility/correlation-finding.d.ts +70 -0
- package/dist/core/telemetry/utility/correlation-finding.js +41 -0
- package/dist/core/telemetry/utility/correlation-input.d.ts +59 -0
- package/dist/core/telemetry/utility/correlation-input.js +139 -0
- package/dist/core/telemetry/utility/correlation-replay.d.ts +24 -0
- package/dist/core/telemetry/utility/correlation-replay.js +46 -0
- package/dist/core/utility/correlation.analysis.d.ts +9 -0
- package/dist/core/utility/correlation.analysis.js +144 -0
- package/dist/core/utility/statistics.d.ts +65 -0
- package/dist/core/utility/statistics.js +141 -0
- package/dist/express/index.d.ts +1 -1
- package/dist/express/index.js +7 -5
- package/dist/express/metrics-websocket.adapter.d.ts +8 -2
- package/dist/express/metrics-websocket.adapter.js +52 -17
- package/dist/express/metrics.adapter.d.ts +1 -0
- package/dist/express/metrics.adapter.js +8 -1
- package/dist/express/metrics.bootstrap.d.ts +6 -1
- package/dist/express/metrics.bootstrap.js +48 -22
- package/dist/express/metrics.dashboard.d.ts +2 -1
- package/dist/express/metrics.dashboard.js +4 -1
- package/dist/express/metrics.middleware.d.ts +2 -1
- package/dist/express/metrics.middleware.js +23 -20
- package/dist/nest/metrics-websocket.adapter.d.ts +19 -3
- package/dist/nest/metrics-websocket.adapter.js +101 -21
- package/dist/nest/metrics.adapter.d.ts +4 -1
- package/dist/nest/metrics.adapter.js +24 -1
- package/dist/nest/metrics.bootstrap.service.d.ts +7 -3
- package/dist/nest/metrics.bootstrap.service.js +56 -21
- package/dist/nest/metrics.interceptor.d.ts +3 -3
- package/dist/nest/metrics.interceptor.js +11 -8
- package/dist/nest/metrics.module.d.ts +3 -0
- package/dist/nest/metrics.module.js +27 -9
- package/dist/runtime/index.d.ts +32 -0
- package/dist/runtime/index.js +104 -0
- package/dist/simulation/simulation.d.ts +5 -16
- package/dist/simulation/simulation.js +5 -8
- package/package.json +2 -1
- package/dashboard-out/_next/static/chunks/20t9qbodfrw_z.js +0 -3
- package/dashboard-out/_next/static/chunks/23soo6qkun-yv.js +0 -1
- package/dashboard-out/_next/static/chunks/3xoopocxi2vi8.css +0 -3
- /package/dashboard-out/_next/static/{u8vU2V6fhASeGVCpWU3eW → 17egwqgk36_DSRF55LJZG}/_buildManifest.js +0 -0
- /package/dashboard-out/_next/static/{u8vU2V6fhASeGVCpWU3eW → 17egwqgk36_DSRF55LJZG}/_clientMiddlewareManifest.js +0 -0
- /package/dashboard-out/_next/static/{u8vU2V6fhASeGVCpWU3eW → 17egwqgk36_DSRF55LJZG}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.SegmentIndex = void 0;
|
|
40
|
+
const fsp = __importStar(require("fs/promises"));
|
|
41
|
+
const path_1 = __importDefault(require("path"));
|
|
42
|
+
const file_buffer_1 = require("../utility/file-buffer");
|
|
43
|
+
const persistence_writer_1 = require("./persistence-writer");
|
|
44
|
+
const logger_1 = __importDefault(require("../../telemetry/logger"));
|
|
45
|
+
const bucket_metric_1 = require("../stores/bucket-metric");
|
|
46
|
+
const CATEGORY = 'metric_buckets';
|
|
47
|
+
const FILE_RE = /^(\d{4}-\d{2}-\d{2})\.ndjson(\.gz)?$/;
|
|
48
|
+
const todayUtc = () => new Date().toISOString().slice(0, 10);
|
|
49
|
+
function touch(entries, date, b) {
|
|
50
|
+
const startMs = Date.parse(b.startTime);
|
|
51
|
+
const endMs = Date.parse(b.endTime);
|
|
52
|
+
if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const e = entries.get(b.sessionId);
|
|
56
|
+
if (!e) {
|
|
57
|
+
entries.set(b.sessionId, {
|
|
58
|
+
date, sessionId: b.sessionId,
|
|
59
|
+
minSequence: b.sequence, maxSequence: b.sequence,
|
|
60
|
+
minStartMs: startMs, maxEndMs: endMs,
|
|
61
|
+
count: 1
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
e.minSequence = Math.min(e.minSequence, b.sequence);
|
|
66
|
+
e.maxSequence = Math.max(e.maxSequence, b.sequence);
|
|
67
|
+
e.minStartMs = Math.min(e.minStartMs, startMs);
|
|
68
|
+
e.maxEndMs = Math.max(e.maxEndMs, endMs);
|
|
69
|
+
e.count++;
|
|
70
|
+
}
|
|
71
|
+
class SegmentIndex {
|
|
72
|
+
baseDir;
|
|
73
|
+
dir;
|
|
74
|
+
byDate = new Map();
|
|
75
|
+
dirty = new Set();
|
|
76
|
+
constructor(baseDir) {
|
|
77
|
+
this.baseDir = baseDir;
|
|
78
|
+
this.dir = path_1.default.join(baseDir, CATEGORY);
|
|
79
|
+
}
|
|
80
|
+
async rebuild() {
|
|
81
|
+
this.byDate.clear();
|
|
82
|
+
this.dirty.clear();
|
|
83
|
+
const files = await fsp.readdir(this.dir).catch(() => []);
|
|
84
|
+
const dates = new Set();
|
|
85
|
+
for (const f of files) {
|
|
86
|
+
const m = FILE_RE.exec(f);
|
|
87
|
+
if (m)
|
|
88
|
+
dates.add(m[1]);
|
|
89
|
+
}
|
|
90
|
+
const today = todayUtc();
|
|
91
|
+
for (const date of [...dates].sort()) {
|
|
92
|
+
try {
|
|
93
|
+
await this.loadOrScan(date, date < today);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
logger_1.default.error(`SegmentIndex: failed to index ${date}: ${err}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
noteWritten(bucket) {
|
|
101
|
+
const date = bucket.endTime.slice(0, 10);
|
|
102
|
+
let entries = this.byDate.get(date);
|
|
103
|
+
if (!entries) {
|
|
104
|
+
entries = new Map();
|
|
105
|
+
this.byDate.set(date, entries);
|
|
106
|
+
}
|
|
107
|
+
touch(entries, date, bucket);
|
|
108
|
+
this.dirty.add(date);
|
|
109
|
+
}
|
|
110
|
+
find(sessionId, fromMs, toMs) {
|
|
111
|
+
const dates = [];
|
|
112
|
+
for (const [date, entries] of this.byDate) {
|
|
113
|
+
const e = entries.get(sessionId);
|
|
114
|
+
if (e && e.minStartMs <= toMs && e.maxEndMs >= fromMs) {
|
|
115
|
+
dates.push(date);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return dates.sort();
|
|
119
|
+
}
|
|
120
|
+
async persistClosed() {
|
|
121
|
+
const today = todayUtc();
|
|
122
|
+
for (const date of [...this.dirty]) {
|
|
123
|
+
if (date >= today)
|
|
124
|
+
continue;
|
|
125
|
+
try {
|
|
126
|
+
await this.persist(date);
|
|
127
|
+
this.dirty.delete(date);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
logger_1.default.error(`SegmentIndex: failed to persist index for ${date}: ${err}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
indexPath(date) {
|
|
135
|
+
return path_1.default.join(this.dir, `${date}.index.json`);
|
|
136
|
+
}
|
|
137
|
+
async resolveExistingFile(category, date) {
|
|
138
|
+
const dir = path_1.default.join(this.baseDir, category);
|
|
139
|
+
const plain = path_1.default.join(dir, `${date}.ndjson`);
|
|
140
|
+
const gz = `${plain}.gz`;
|
|
141
|
+
if (await fsp.access(plain).then(() => true).catch(() => false))
|
|
142
|
+
return plain;
|
|
143
|
+
if (await fsp.access(gz).then(() => true).catch(() => false))
|
|
144
|
+
return gz;
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
async loadOrScan(date, closed) {
|
|
148
|
+
const file = await this.resolveExistingFile(CATEGORY, date);
|
|
149
|
+
if (!file)
|
|
150
|
+
return;
|
|
151
|
+
const size = (await fsp.stat(file)).size;
|
|
152
|
+
if (closed) {
|
|
153
|
+
const cached = await this.tryLoad(date, path_1.default.basename(file), size);
|
|
154
|
+
if (cached) {
|
|
155
|
+
this.byDate.set(date, cached);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const entries = await this.scan(file, date);
|
|
160
|
+
this.byDate.set(date, entries);
|
|
161
|
+
if (closed) {
|
|
162
|
+
await this.persist(date).catch(err => logger_1.default.error(`SegmentIndex: persist failed for ${date}: ${err}`));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
this.dirty.add(date);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async tryLoad(date, fileName, fileSize) {
|
|
169
|
+
try {
|
|
170
|
+
const raw = JSON.parse(await fsp.readFile(this.indexPath(date), 'utf-8'));
|
|
171
|
+
if (raw.version !== 1 || raw.date !== date || raw.fileName !== fileName || raw.fileSize !== fileSize) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return new Map(raw.entries.map(e => [e.sessionId, e]));
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async scan(file, date) {
|
|
181
|
+
const entries = new Map();
|
|
182
|
+
for await (const rec of (0, persistence_writer_1.dedupeRecords)((0, file_buffer_1.readRecords)(file))) {
|
|
183
|
+
const b = rec.payload;
|
|
184
|
+
if (!b || b.schemaVersion !== bucket_metric_1.BUCKET_SCHEMA_VERSION || typeof b.sessionId !== 'string')
|
|
185
|
+
continue;
|
|
186
|
+
touch(entries, date, b);
|
|
187
|
+
}
|
|
188
|
+
return entries;
|
|
189
|
+
}
|
|
190
|
+
async persist(date) {
|
|
191
|
+
const entries = this.byDate.get(date);
|
|
192
|
+
const file = await this.resolveExistingFile(CATEGORY, date);
|
|
193
|
+
if (!entries || !file)
|
|
194
|
+
return;
|
|
195
|
+
const size = (await fsp.stat(file)).size;
|
|
196
|
+
const data = {
|
|
197
|
+
version: 1,
|
|
198
|
+
date,
|
|
199
|
+
fileName: path_1.default.basename(file),
|
|
200
|
+
fileSize: size,
|
|
201
|
+
entries: [...entries.values()]
|
|
202
|
+
};
|
|
203
|
+
await (0, file_buffer_1.writeJSONAtomic)(this.indexPath(date), data);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
exports.SegmentIndex = SegmentIndex;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SessionRecord } from '../../domain';
|
|
2
|
+
import type { PersistenceWriter } from './persistence-writer';
|
|
3
|
+
export interface SessionWindow {
|
|
4
|
+
start: Date;
|
|
5
|
+
end: Date;
|
|
6
|
+
}
|
|
7
|
+
export declare class SessionRegistry {
|
|
8
|
+
private readonly manifestPath;
|
|
9
|
+
private readonly writer;
|
|
10
|
+
private manifest;
|
|
11
|
+
private dirty;
|
|
12
|
+
private _current;
|
|
13
|
+
private _recoveredFromCrash;
|
|
14
|
+
constructor(manifestPath: string, writer: PersistenceWriter);
|
|
15
|
+
get current(): SessionRecord | null;
|
|
16
|
+
get all(): readonly SessionRecord[];
|
|
17
|
+
get recoveredFromCrash(): boolean;
|
|
18
|
+
start(sessionId?: string): Promise<SessionRecord>;
|
|
19
|
+
notePersisted(committedSequence?: number): void;
|
|
20
|
+
heartbeat(): Promise<void>;
|
|
21
|
+
complete(): Promise<void>;
|
|
22
|
+
static windowEnd(s: SessionRecord): string | null;
|
|
23
|
+
static window(s: SessionRecord): SessionWindow;
|
|
24
|
+
static enumerateDates({ start, end }: SessionWindow): string[];
|
|
25
|
+
static enumerateHours({ start, end }: SessionWindow): string[];
|
|
26
|
+
private save;
|
|
27
|
+
private migrate;
|
|
28
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionRegistry = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const index_1 = require("../utility/index");
|
|
6
|
+
const emptyManifest = () => ({ version: 2, sessions: [] });
|
|
7
|
+
class SessionRegistry {
|
|
8
|
+
manifestPath;
|
|
9
|
+
writer;
|
|
10
|
+
manifest = emptyManifest();
|
|
11
|
+
dirty = false;
|
|
12
|
+
_current = null;
|
|
13
|
+
_recoveredFromCrash = false;
|
|
14
|
+
constructor(manifestPath, writer) {
|
|
15
|
+
this.manifestPath = manifestPath;
|
|
16
|
+
this.writer = writer;
|
|
17
|
+
}
|
|
18
|
+
get current() { return this._current; }
|
|
19
|
+
get all() { return this.manifest.sessions; }
|
|
20
|
+
get recoveredFromCrash() { return this._recoveredFromCrash; }
|
|
21
|
+
async start(sessionId = (0, crypto_1.randomUUID)()) {
|
|
22
|
+
const raw = await (0, index_1.tryOrDefault)(() => (0, index_1.readJSON)(this.manifestPath), null);
|
|
23
|
+
this.manifest = this.migrate(raw);
|
|
24
|
+
for (const s of this.manifest.sessions) {
|
|
25
|
+
if (s.status === 'RUNNING') {
|
|
26
|
+
s.status = 'INTERRUPTED';
|
|
27
|
+
s.endedAt = s.lastPersistedAt ?? s.startedAt;
|
|
28
|
+
this._recoveredFromCrash = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const number = this.manifest.sessions.reduce((m, s) => Math.max(m, s.sessionNumber), 0) + 1;
|
|
32
|
+
this._current = {
|
|
33
|
+
sessionId: sessionId,
|
|
34
|
+
sessionNumber: number,
|
|
35
|
+
startedAt: new Date().toISOString(),
|
|
36
|
+
endedAt: null,
|
|
37
|
+
lastPersistedAt: null,
|
|
38
|
+
lastCommittedSequence: 0,
|
|
39
|
+
status: 'RUNNING',
|
|
40
|
+
};
|
|
41
|
+
this.manifest.sessions.push(this._current);
|
|
42
|
+
await this.save('manifest: session start');
|
|
43
|
+
return this._current;
|
|
44
|
+
}
|
|
45
|
+
notePersisted(committedSequence) {
|
|
46
|
+
if (!this._current)
|
|
47
|
+
return;
|
|
48
|
+
this._current.lastPersistedAt = new Date().toISOString();
|
|
49
|
+
if (committedSequence !== undefined) {
|
|
50
|
+
this._current.lastCommittedSequence = Math.max(this._current.lastCommittedSequence, committedSequence);
|
|
51
|
+
}
|
|
52
|
+
this.dirty = true;
|
|
53
|
+
}
|
|
54
|
+
async heartbeat() {
|
|
55
|
+
if (!this.dirty)
|
|
56
|
+
return;
|
|
57
|
+
this.dirty = false;
|
|
58
|
+
await this.save('manifest: heartbeat').catch(() => { this.dirty = true; });
|
|
59
|
+
}
|
|
60
|
+
async complete() {
|
|
61
|
+
if (!this._current)
|
|
62
|
+
return;
|
|
63
|
+
await this.writer.whenIdle();
|
|
64
|
+
const now = new Date().toISOString();
|
|
65
|
+
this._current.status = 'COMPLETED';
|
|
66
|
+
this._current.endedAt = now;
|
|
67
|
+
this._current.lastPersistedAt = now;
|
|
68
|
+
this.dirty = false;
|
|
69
|
+
await this.save('manifest: session complete');
|
|
70
|
+
}
|
|
71
|
+
static windowEnd(s) {
|
|
72
|
+
return s.status === 'RUNNING' ? null : (s.endedAt ?? s.lastPersistedAt);
|
|
73
|
+
}
|
|
74
|
+
static window(s) {
|
|
75
|
+
const end = SessionRegistry.windowEnd(s);
|
|
76
|
+
return {
|
|
77
|
+
start: new Date(s.startedAt),
|
|
78
|
+
end: end ? new Date(end) : new Date(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
static enumerateDates({ start, end }) {
|
|
82
|
+
const dates = [];
|
|
83
|
+
const cursor = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate()));
|
|
84
|
+
const last = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate()));
|
|
85
|
+
while (cursor <= last) {
|
|
86
|
+
dates.push(cursor.toISOString().slice(0, 10));
|
|
87
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
88
|
+
}
|
|
89
|
+
return dates;
|
|
90
|
+
}
|
|
91
|
+
static enumerateHours({ start, end }) {
|
|
92
|
+
const hours = [];
|
|
93
|
+
const cursor = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate(), start.getUTCHours()));
|
|
94
|
+
const last = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate(), end.getUTCHours()));
|
|
95
|
+
while (cursor <= last) {
|
|
96
|
+
hours.push(cursor.toISOString());
|
|
97
|
+
cursor.setUTCHours(cursor.getUTCHours() + 1);
|
|
98
|
+
}
|
|
99
|
+
return hours;
|
|
100
|
+
}
|
|
101
|
+
save(description) {
|
|
102
|
+
const snapshot = structuredClone(this.manifest);
|
|
103
|
+
return this.writer.enqueueTask(description, () => (0, index_1.writeJSONAtomic)(this.manifestPath, snapshot));
|
|
104
|
+
}
|
|
105
|
+
migrate(raw) {
|
|
106
|
+
if (raw?.version === 2 && Array.isArray(raw.sessions)) {
|
|
107
|
+
for (const s of raw.sessions) {
|
|
108
|
+
if (typeof s.lastCommittedSequence !== 'number') {
|
|
109
|
+
s.lastCommittedSequence = typeof s.lastCommitedSequence === 'number' ? s.lastCommitedSequence : 0;
|
|
110
|
+
}
|
|
111
|
+
delete s.lastCommitedSequence;
|
|
112
|
+
}
|
|
113
|
+
return raw;
|
|
114
|
+
}
|
|
115
|
+
const sessions = (raw?.sessionHistory ?? []).map((h) => ({
|
|
116
|
+
sessionId: `legacy-${h.sessionNumber}`,
|
|
117
|
+
sessionNumber: h.sessionNumber,
|
|
118
|
+
startedAt: h.startedAt,
|
|
119
|
+
endedAt: h.endedAt ?? null,
|
|
120
|
+
lastPersistedAt: h.endedAt ?? null,
|
|
121
|
+
lastCommittedSequence: 0,
|
|
122
|
+
status: h.endedAt ? 'COMPLETED' : 'INTERRUPTED',
|
|
123
|
+
}));
|
|
124
|
+
return { version: 2, sessions };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.SessionRegistry = SessionRegistry;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Incident, SessionRecord, SessionSummary } from "../../domain";
|
|
2
|
+
import type { TelemetryQueryService } from "../../telemetry/telemetry-query.service";
|
|
3
|
+
export declare function buildSessionSummary(queries: TelemetryQueryService, session: SessionRecord, windowHours: number, slowLatencyMs: number, incidents?: Incident[]): Promise<SessionSummary>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSessionSummary = buildSessionSummary;
|
|
4
|
+
const session_registry_1 = require("./session-registry");
|
|
5
|
+
const bucket_metric_1 = require("../stores/bucket-metric");
|
|
6
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
7
|
+
const TARGET_POINTS = 60;
|
|
8
|
+
const TOP_ROUTES = 5;
|
|
9
|
+
const RESOLUTIONS_MS = [
|
|
10
|
+
1_000, 5_000, 10_000, 30_000,
|
|
11
|
+
60_000, 5 * 60_000, 10 * 60_000, 15 * 60_000, 30 * 60_000,
|
|
12
|
+
HOUR_MS, 3 * HOUR_MS, 6 * HOUR_MS, 12 * HOUR_MS, 24 * HOUR_MS,
|
|
13
|
+
];
|
|
14
|
+
function pickResolution(spanMs) {
|
|
15
|
+
return RESOLUTIONS_MS.find(r => spanMs / r <= TARGET_POINTS) ?? RESOLUTIONS_MS[RESOLUTIONS_MS.length - 1];
|
|
16
|
+
}
|
|
17
|
+
const rate = (part, total) => total > 0 ? part / total : 0;
|
|
18
|
+
async function buildSessionSummary(queries, session, windowHours, slowLatencyMs, incidents = []) {
|
|
19
|
+
const window = session_registry_1.SessionRegistry.window(session);
|
|
20
|
+
const startCandidate = new Date(window.end.getTime() - windowHours * HOUR_MS);
|
|
21
|
+
if (startCandidate > window.start)
|
|
22
|
+
window.start = startCandidate;
|
|
23
|
+
const resolutionMs = pickResolution(window.end.getTime() - window.start.getTime());
|
|
24
|
+
const result = await queries.query({
|
|
25
|
+
sessionId: session.sessionId,
|
|
26
|
+
from: window.start.toISOString(),
|
|
27
|
+
to: window.end.toISOString(),
|
|
28
|
+
resolutionMs,
|
|
29
|
+
includeEndpoints: true,
|
|
30
|
+
consistency: session.status === 'RUNNING' ? 'live' : 'committed',
|
|
31
|
+
});
|
|
32
|
+
const routes = new Map();
|
|
33
|
+
const points = result.points.map(p => {
|
|
34
|
+
for (const e of p.endpoints ?? []) {
|
|
35
|
+
const key = `${e.method}:${e.route}`;
|
|
36
|
+
let r = routes.get(key);
|
|
37
|
+
if (!r) {
|
|
38
|
+
r = { method: e.method, route: e.route, agg: (0, bucket_metric_1.createAggregate)() };
|
|
39
|
+
routes.set(key, r);
|
|
40
|
+
}
|
|
41
|
+
(0, bucket_metric_1.mergeInto)(r.agg, e);
|
|
42
|
+
}
|
|
43
|
+
const m = (0, bucket_metric_1.deriveMetrics)(p.requests, p.durationMs);
|
|
44
|
+
return {
|
|
45
|
+
startTime: p.startTime,
|
|
46
|
+
durationMs: p.durationMs,
|
|
47
|
+
requestCount: m.requestCount,
|
|
48
|
+
rps: m.rps,
|
|
49
|
+
maxRps: p.maxRps,
|
|
50
|
+
p95: m.p95,
|
|
51
|
+
serverErrorRate: rate(m.serverErrorCount, m.requestCount),
|
|
52
|
+
clientErrorCount: m.clientErrorCount,
|
|
53
|
+
serverErrorCount: m.serverErrorCount,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
const topRoutes = [...routes.values()]
|
|
57
|
+
.map(({ method, route, agg }) => ({
|
|
58
|
+
method,
|
|
59
|
+
route,
|
|
60
|
+
requestCount: agg.requestCount,
|
|
61
|
+
impactedRequests: agg.serverErrorCount + (0, bucket_metric_1.countSlowerThan)(agg, slowLatencyMs),
|
|
62
|
+
p95: (0, bucket_metric_1.deriveMetrics)(agg, 1000).p95,
|
|
63
|
+
serverErrorRate: rate(agg.serverErrorCount, agg.requestCount),
|
|
64
|
+
}))
|
|
65
|
+
.filter(r => r.impactedRequests > 0)
|
|
66
|
+
.sort((a, b) => b.impactedRequests - a.impactedRequests || b.requestCount - a.requestCount)
|
|
67
|
+
.slice(0, TOP_ROUTES);
|
|
68
|
+
let missingIntervals = 0;
|
|
69
|
+
let conflicts = 0;
|
|
70
|
+
for (const issue of result.issues) {
|
|
71
|
+
if (issue.type === 'GAP')
|
|
72
|
+
missingIntervals += issue.toSequence - issue.fromSequence + 1;
|
|
73
|
+
else if (issue.type === 'CONFLICT')
|
|
74
|
+
conflicts++;
|
|
75
|
+
}
|
|
76
|
+
const t = result.total.derived;
|
|
77
|
+
return {
|
|
78
|
+
sessionId: session.sessionId,
|
|
79
|
+
sessionNumber: session.sessionNumber,
|
|
80
|
+
status: session.status,
|
|
81
|
+
startedAt: session.startedAt,
|
|
82
|
+
endedAt: session.endedAt,
|
|
83
|
+
windowStart: window.start.toISOString(),
|
|
84
|
+
windowEnd: window.end.toISOString(),
|
|
85
|
+
measuredMs: result.total.durationMs,
|
|
86
|
+
traffic: {
|
|
87
|
+
requestCount: t.requestCount,
|
|
88
|
+
avgRps: t.rps,
|
|
89
|
+
peakRps: result.total.maxRps,
|
|
90
|
+
},
|
|
91
|
+
latency: {
|
|
92
|
+
p50: t.p50,
|
|
93
|
+
p95: t.p95,
|
|
94
|
+
p99: t.p99,
|
|
95
|
+
max: t.maxLatency,
|
|
96
|
+
},
|
|
97
|
+
errors: {
|
|
98
|
+
serverErrorCount: t.serverErrorCount,
|
|
99
|
+
serverErrorRate: rate(t.serverErrorCount, t.requestCount),
|
|
100
|
+
clientErrorCount: t.clientErrorCount,
|
|
101
|
+
},
|
|
102
|
+
incidents,
|
|
103
|
+
topRoutes,
|
|
104
|
+
series: { resolutionMs, points },
|
|
105
|
+
quality: {
|
|
106
|
+
bucketCount: result.total.bucketCount,
|
|
107
|
+
missingIntervals,
|
|
108
|
+
conflicts,
|
|
109
|
+
complete: result.complete,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type ReadonlyConfig } from "../../config";
|
|
2
|
+
import type { ApplicationEvent, SessionSummary, SessionRecord, SessionManifest } from "../domain";
|
|
3
|
+
import { PersistenceWriter, type StorageStatus } from './persistence/persistence-writer';
|
|
4
|
+
import { type MetricBucket } from "./stores/bucket-metric";
|
|
5
|
+
import type { CommittedBucketSource, MetricsQuery, QueryIssue, TelemetryQueryService } from "../telemetry/telemetry-query.service";
|
|
6
|
+
import type { CorrelationFinding } from "../telemetry/utility/correlation-finding";
|
|
7
|
+
type HttpMetricRecord = {
|
|
8
|
+
method: string;
|
|
9
|
+
path: string;
|
|
10
|
+
statusCode: number;
|
|
11
|
+
durationMs: number;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
};
|
|
14
|
+
export declare class PersistenceRepository {
|
|
15
|
+
private readonly conf;
|
|
16
|
+
private httpBuffer;
|
|
17
|
+
private readonly enabled;
|
|
18
|
+
private readonly maxBufferSize;
|
|
19
|
+
private readonly baseDir;
|
|
20
|
+
private readonly persistRawRequests;
|
|
21
|
+
private readonly heartbeatIntervalMs;
|
|
22
|
+
private readonly writer;
|
|
23
|
+
private readonly registry;
|
|
24
|
+
private readonly ready;
|
|
25
|
+
private heartbeatTimer;
|
|
26
|
+
private shutdownPromise;
|
|
27
|
+
private closing;
|
|
28
|
+
private readonly inFlight;
|
|
29
|
+
readonly sessionId: `${string}-${string}-${string}-${string}-${string}`;
|
|
30
|
+
private readonly segments;
|
|
31
|
+
private queries;
|
|
32
|
+
constructor(conf: ReadonlyConfig);
|
|
33
|
+
attachQueryService(q: TelemetryQueryService): void;
|
|
34
|
+
get isEnabled(): boolean;
|
|
35
|
+
get bucketSource(): CommittedBucketSource;
|
|
36
|
+
private initSession;
|
|
37
|
+
private track;
|
|
38
|
+
private drainInFlight;
|
|
39
|
+
private enqueue;
|
|
40
|
+
onHttpRequest(metric: HttpMetricRecord): void;
|
|
41
|
+
flushHttpBuffer(): Promise<void>;
|
|
42
|
+
onBucketClosed(bucket: MetricBucket): Promise<boolean>;
|
|
43
|
+
saveCorrelationFinding(finding: CorrelationFinding): Promise<boolean>;
|
|
44
|
+
onApplicationEvent(event: ApplicationEvent): Promise<void>;
|
|
45
|
+
getStorageStatus(): {
|
|
46
|
+
status: StorageStatus;
|
|
47
|
+
lostRecords: number;
|
|
48
|
+
pending: number;
|
|
49
|
+
lastError: PersistenceWriter['lastError'];
|
|
50
|
+
};
|
|
51
|
+
getSessionManifest(): Promise<SessionManifest>;
|
|
52
|
+
getSessionRecord(): SessionRecord | null;
|
|
53
|
+
findSession(sessionNumber: number): Promise<SessionRecord | null>;
|
|
54
|
+
private resolveExistingFile;
|
|
55
|
+
private requireQueries;
|
|
56
|
+
sessionQuery(session: SessionRecord): MetricsQuery;
|
|
57
|
+
getSessionSummary(sessionNumber: number): Promise<SessionSummary | null>;
|
|
58
|
+
private sessionIncidents;
|
|
59
|
+
findCorrelationFinding(findingId: string): Promise<CorrelationFinding | null>;
|
|
60
|
+
readBuckets(sessionId: string, fromMs: number, toMs: number, onIssue: (i: QueryIssue) => void): AsyncGenerator<MetricBucket>;
|
|
61
|
+
shutdown(): Promise<void>;
|
|
62
|
+
private archiveOldCategory;
|
|
63
|
+
archiveAllCategories(): void;
|
|
64
|
+
private writeChunk;
|
|
65
|
+
private streamJsonArray;
|
|
66
|
+
private sessionRecords;
|
|
67
|
+
streamSessionExport(out: NodeJS.WritableStream, sessionNumber: number): Promise<boolean>;
|
|
68
|
+
}
|
|
69
|
+
export {};
|