@uptimizr/collector-server 2.0.1 → 2.2.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/AGENTS.md +187 -11
- package/README.md +91 -7
- package/dist/__tests__/support/registryRequests.d.ts +2 -1
- package/dist/__tests__/support/registryRequests.d.ts.map +1 -1
- package/dist/__tests__/support/registryRequests.js +17 -1
- package/dist/__tests__/support/registryRequests.js.map +1 -1
- package/dist/__tests__/support/subscriptionFixtures.d.ts +63 -0
- package/dist/__tests__/support/subscriptionFixtures.d.ts.map +1 -0
- package/dist/__tests__/support/subscriptionFixtures.js +17 -0
- package/dist/__tests__/support/subscriptionFixtures.js.map +1 -0
- package/dist/agentReport.d.ts +256 -0
- package/dist/agentReport.d.ts.map +1 -0
- package/dist/agentReport.js +861 -0
- package/dist/agentReport.js.map +1 -0
- package/dist/app.d.ts +13 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +112 -5
- package/dist/app.js.map +1 -1
- package/dist/audit.d.ts +5 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +24 -4
- package/dist/audit.js.map +1 -1
- package/dist/auth.d.ts +41 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +47 -0
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +133 -13
- package/dist/cli.js.map +1 -1
- package/dist/cliStore.d.ts +31 -2
- package/dist/cliStore.d.ts.map +1 -1
- package/dist/cliStore.js +34 -0
- package/dist/cliStore.js.map +1 -1
- package/dist/clickhouseStore.d.ts.map +1 -1
- package/dist/clickhouseStore.js +44 -2
- package/dist/clickhouseStore.js.map +1 -1
- package/dist/config.d.ts +49 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -0
- package/dist/config.js.map +1 -1
- package/dist/connectionLimiter.d.ts +23 -0
- package/dist/connectionLimiter.d.ts.map +1 -0
- package/dist/connectionLimiter.js +24 -0
- package/dist/connectionLimiter.js.map +1 -0
- package/dist/duckdbStore.d.ts.map +1 -1
- package/dist/duckdbStore.js +44 -1
- package/dist/duckdbStore.js.map +1 -1
- package/dist/internalDispatch.d.ts +36 -0
- package/dist/internalDispatch.d.ts.map +1 -0
- package/dist/internalDispatch.js +50 -0
- package/dist/internalDispatch.js.map +1 -0
- package/dist/memoryStore.d.ts.map +1 -1
- package/dist/memoryStore.js +314 -1
- package/dist/memoryStore.js.map +1 -1
- package/dist/mssqlStore.d.ts.map +1 -1
- package/dist/mssqlStore.js +44 -2
- package/dist/mssqlStore.js.map +1 -1
- package/dist/postgresStore.d.ts.map +1 -1
- package/dist/postgresStore.js +44 -2
- package/dist/postgresStore.js.map +1 -1
- package/dist/projectMetadata.d.ts +82 -0
- package/dist/projectMetadata.d.ts.map +1 -0
- package/dist/projectMetadata.js +50 -0
- package/dist/projectMetadata.js.map +1 -0
- package/dist/routes/context.d.ts +135 -0
- package/dist/routes/context.d.ts.map +1 -0
- package/dist/routes/context.js +306 -0
- package/dist/routes/context.js.map +1 -0
- package/dist/routes/insights.d.ts +36 -0
- package/dist/routes/insights.d.ts.map +1 -0
- package/dist/routes/insights.js +810 -0
- package/dist/routes/insights.js.map +1 -0
- package/dist/routes/live.d.ts +8 -0
- package/dist/routes/live.d.ts.map +1 -1
- package/dist/routes/live.js +8 -7
- package/dist/routes/live.js.map +1 -1
- package/dist/routes/mcp.d.ts +36 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +262 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/meta.d.ts +16 -2
- package/dist/routes/meta.d.ts.map +1 -1
- package/dist/routes/meta.js +699 -10
- package/dist/routes/meta.js.map +1 -1
- package/dist/routes/metadata.d.ts +31 -0
- package/dist/routes/metadata.d.ts.map +1 -0
- package/dist/routes/metadata.js +163 -0
- package/dist/routes/metadata.js.map +1 -0
- package/dist/routes/narrative.d.ts +36 -0
- package/dist/routes/narrative.d.ts.map +1 -0
- package/dist/routes/narrative.js +95 -0
- package/dist/routes/narrative.js.map +1 -0
- package/dist/routes/panels.d.ts +44 -0
- package/dist/routes/panels.d.ts.map +1 -0
- package/dist/routes/panels.js +108 -0
- package/dist/routes/panels.js.map +1 -0
- package/dist/routes/query-dsl.d.ts +50 -0
- package/dist/routes/query-dsl.d.ts.map +1 -0
- package/dist/routes/query-dsl.js +359 -0
- package/dist/routes/query-dsl.js.map +1 -0
- package/dist/routes/query.d.ts +36 -0
- package/dist/routes/query.d.ts.map +1 -1
- package/dist/routes/query.js +101 -13
- package/dist/routes/query.js.map +1 -1
- package/dist/routes/subscriptions.d.ts +52 -0
- package/dist/routes/subscriptions.d.ts.map +1 -0
- package/dist/routes/subscriptions.js +290 -0
- package/dist/routes/subscriptions.js.map +1 -0
- package/dist/store.d.ts +113 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/subscriptions/cli.d.ts +36 -0
- package/dist/subscriptions/cli.d.ts.map +1 -0
- package/dist/subscriptions/cli.js +143 -0
- package/dist/subscriptions/cli.js.map +1 -0
- package/dist/subscriptions/delivery.d.ts +101 -0
- package/dist/subscriptions/delivery.d.ts.map +1 -0
- package/dist/subscriptions/delivery.js +165 -0
- package/dist/subscriptions/delivery.js.map +1 -0
- package/dist/subscriptions/evaluate.d.ts +119 -0
- package/dist/subscriptions/evaluate.d.ts.map +1 -0
- package/dist/subscriptions/evaluate.js +326 -0
- package/dist/subscriptions/evaluate.js.map +1 -0
- package/dist/subscriptions/scheduler.d.ts +97 -0
- package/dist/subscriptions/scheduler.d.ts.map +1 -0
- package/dist/subscriptions/scheduler.js +332 -0
- package/dist/subscriptions/scheduler.js.map +1 -0
- package/dist/subscriptions/stream.d.ts +40 -0
- package/dist/subscriptions/stream.d.ts.map +1 -0
- package/dist/subscriptions/stream.js +38 -0
- package/dist/subscriptions/stream.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +25 -0
- package/dist/version.js.map +1 -0
- package/dist/webhookSignature.d.ts +64 -0
- package/dist/webhookSignature.d.ts.map +1 -0
- package/dist/webhookSignature.js +91 -0
- package/dist/webhookSignature.js.map +1 -0
- package/llms.txt +39 -2
- package/package.json +17 -14
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { ANOMALY_MIN_TRAILING, ANOMALY_TRAILING_BUCKETS, BUCKET_SECONDS, bucketMeasureFor, computeBaseline, computeMover, detectAnomalies, floorToBucket, inWindow, isBucketableMetric, rollupWindow, summarizeRows, } from "@uptimizr/db";
|
|
2
|
+
import { parseDurationMs } from "@uptimizr/schema";
|
|
3
|
+
/** Windows up to this long default to hourly buckets; longer ones to daily. */
|
|
4
|
+
const HOURLY_WINDOW_CEILING_MS = 7 * 86_400_000;
|
|
5
|
+
/** Resolve the series grain: the declared one, else hourly for short windows. */
|
|
6
|
+
export function bucketFor(sub) {
|
|
7
|
+
if (sub.evaluate.bucket != null)
|
|
8
|
+
return sub.evaluate.bucket;
|
|
9
|
+
const windowMs = parseDurationMs(sub.evaluate.window) ?? HOURLY_WINDOW_CEILING_MS;
|
|
10
|
+
return windowMs <= HOURLY_WINDOW_CEILING_MS ? "hour" : "day";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The window one evaluation measures: `[floor(now − window), now)`.
|
|
14
|
+
*
|
|
15
|
+
* `since` is floored so the series is bucket-aligned; `until` is not, so the
|
|
16
|
+
* partial bucket containing *this minute* is included. See the module note.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveWindow(windowMs, bucket, now) {
|
|
19
|
+
const width = BUCKET_SECONDS[bucket] * 1000;
|
|
20
|
+
const since = floorToBucket(now - Math.max(windowMs, width), bucket);
|
|
21
|
+
return { since, until: now };
|
|
22
|
+
}
|
|
23
|
+
/** Compare two numbers with a subscription's operator. */
|
|
24
|
+
export function compare(left, op, right) {
|
|
25
|
+
switch (op) {
|
|
26
|
+
case "<":
|
|
27
|
+
return left < right;
|
|
28
|
+
case "<=":
|
|
29
|
+
return left <= right;
|
|
30
|
+
case ">":
|
|
31
|
+
return left > right;
|
|
32
|
+
case ">=":
|
|
33
|
+
return left >= right;
|
|
34
|
+
case "==":
|
|
35
|
+
return left === right;
|
|
36
|
+
case "!=":
|
|
37
|
+
return left !== right;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Sum the denominators of a window's rows. */
|
|
41
|
+
function sampleOf(rows) {
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (const row of rows)
|
|
44
|
+
total += Number(row.sample_size) || 0;
|
|
45
|
+
return total;
|
|
46
|
+
}
|
|
47
|
+
/** The non-null values of a window's rows, in bucket order. */
|
|
48
|
+
function valuesOf(rows) {
|
|
49
|
+
return rows.filter((row) => row.value != null).map((row) => Number(row.value));
|
|
50
|
+
}
|
|
51
|
+
/** Round to the precision the firing payload reports numbers at. */
|
|
52
|
+
function round(value) {
|
|
53
|
+
return value == null || !Number.isFinite(value) ? null : Math.round(value * 1e6) / 1e6;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The column a `threshold` predicate on `metric` may name — its registry
|
|
57
|
+
* `comparable.primary`, or `null` when the metric declares none.
|
|
58
|
+
*/
|
|
59
|
+
export function thresholdColumnFor(metric) {
|
|
60
|
+
return metric.comparable?.primary ?? null;
|
|
61
|
+
}
|
|
62
|
+
/** Whether a metric can back a store-evaluated subscription at all. */
|
|
63
|
+
export function isSubscribableMetric(metricId) {
|
|
64
|
+
return isBucketableMetric(metricId);
|
|
65
|
+
}
|
|
66
|
+
/** The effective `minSample` for a threshold: the predicate's, else the registry's. */
|
|
67
|
+
function minSampleFor(sub, metric) {
|
|
68
|
+
if (sub.predicate.kind === "threshold" && sub.predicate.minSample != null) {
|
|
69
|
+
return sub.predicate.minSample;
|
|
70
|
+
}
|
|
71
|
+
return metric.comparable?.minSample ?? 1;
|
|
72
|
+
}
|
|
73
|
+
/** A not-fired result carrying the numbers the caller still wants to see. */
|
|
74
|
+
function quiet(reason, window, bucket, series, extra = {}) {
|
|
75
|
+
return {
|
|
76
|
+
fired: false,
|
|
77
|
+
reason,
|
|
78
|
+
window,
|
|
79
|
+
value: null,
|
|
80
|
+
expected: null,
|
|
81
|
+
sampleSize: sampleOf(series),
|
|
82
|
+
dimensionValue: null,
|
|
83
|
+
series,
|
|
84
|
+
bucket,
|
|
85
|
+
...extra,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Evaluate one subscription now.
|
|
90
|
+
*
|
|
91
|
+
* Every predicate kind but `presence` issues **one** bucket read; `movers` and
|
|
92
|
+
* `new_value` cover both their windows with a single spanning scan, the same
|
|
93
|
+
* trick `movers` itself uses, so both windows are guaranteed to have seen one
|
|
94
|
+
* snapshot of the data.
|
|
95
|
+
*/
|
|
96
|
+
export async function evaluateSubscription(deps, sub, metric) {
|
|
97
|
+
const now = (deps.now ?? Date.now)();
|
|
98
|
+
const bucket = bucketFor(sub);
|
|
99
|
+
const windowMs = parseDurationMs(sub.evaluate.window) ?? 3_600_000;
|
|
100
|
+
const window = resolveWindow(windowMs, bucket, now);
|
|
101
|
+
const scene = sub.filters.scene;
|
|
102
|
+
const read = (opts) => deps.readBuckets({
|
|
103
|
+
metric: metric.id,
|
|
104
|
+
bucket,
|
|
105
|
+
since: window.since,
|
|
106
|
+
until: window.until,
|
|
107
|
+
scene,
|
|
108
|
+
...opts,
|
|
109
|
+
});
|
|
110
|
+
switch (sub.predicate.kind) {
|
|
111
|
+
case "presence": {
|
|
112
|
+
const active = deps.activeSessions?.() ?? 0;
|
|
113
|
+
const fired = compare(active, sub.predicate.op, sub.predicate.value);
|
|
114
|
+
return {
|
|
115
|
+
fired,
|
|
116
|
+
reason: `${active} live session(s); predicate is presence ${sub.predicate.op} ${sub.predicate.value}`,
|
|
117
|
+
window: { since: now, until: now },
|
|
118
|
+
value: active,
|
|
119
|
+
expected: sub.predicate.value,
|
|
120
|
+
sampleSize: active,
|
|
121
|
+
dimensionValue: null,
|
|
122
|
+
series: [],
|
|
123
|
+
bucket,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
case "threshold": {
|
|
127
|
+
const rows = await read({});
|
|
128
|
+
const values = valuesOf(rows);
|
|
129
|
+
const sampleSize = sampleOf(rows);
|
|
130
|
+
const minSample = minSampleFor(sub, metric);
|
|
131
|
+
if (sampleSize < minSample) {
|
|
132
|
+
return quiet(`window held ${sampleSize} sample(s), below minSample ${minSample}`, window, bucket, rows);
|
|
133
|
+
}
|
|
134
|
+
const rollup = bucketMeasureFor(metric.id)?.rollup ?? "mean";
|
|
135
|
+
const value = rollupWindow(values, rollup);
|
|
136
|
+
if (value == null) {
|
|
137
|
+
return quiet(`no value for ${metric.id} in the window`, window, bucket, rows);
|
|
138
|
+
}
|
|
139
|
+
const fired = compare(value, sub.predicate.op, sub.predicate.value);
|
|
140
|
+
const column = sub.predicate.column;
|
|
141
|
+
return {
|
|
142
|
+
fired,
|
|
143
|
+
reason: fired
|
|
144
|
+
? `${metric.id}.${column} is ${round(value)} — ${sub.predicate.op} ${sub.predicate.value} over ${sub.evaluate.window}`
|
|
145
|
+
: `${metric.id}.${column} is ${round(value)}, which is not ${sub.predicate.op} ${sub.predicate.value}`,
|
|
146
|
+
window,
|
|
147
|
+
value: round(value),
|
|
148
|
+
expected: sub.predicate.value,
|
|
149
|
+
sampleSize,
|
|
150
|
+
dimensionValue: null,
|
|
151
|
+
series: rows,
|
|
152
|
+
bucket,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
case "movers": {
|
|
156
|
+
// One spanning scan covering the current window and the equal one before
|
|
157
|
+
// it, split in TypeScript — half the reads, and one snapshot of the data.
|
|
158
|
+
const reference = { since: window.since - windowMs, until: window.since };
|
|
159
|
+
const rows = await read({ since: reference.since });
|
|
160
|
+
const current = rows.filter((row) => inWindow(row.bucket, window));
|
|
161
|
+
const previous = rows.filter((row) => inWindow(row.bucket, reference));
|
|
162
|
+
const sampleSize = sampleOf(current);
|
|
163
|
+
const minSample = minSampleFor(sub, metric);
|
|
164
|
+
if (sampleSize < minSample) {
|
|
165
|
+
return quiet(`window held ${sampleSize} sample(s), below minSample ${minSample}`, window, bucket, current);
|
|
166
|
+
}
|
|
167
|
+
const mover = computeMover({
|
|
168
|
+
metric: metric.id,
|
|
169
|
+
direction: metric.comparable?.direction ?? "neutral",
|
|
170
|
+
minSample,
|
|
171
|
+
rollup: bucketMeasureFor(metric.id)?.rollup ?? "sum",
|
|
172
|
+
current,
|
|
173
|
+
reference: previous,
|
|
174
|
+
});
|
|
175
|
+
// `deltaPct` is a signed *fraction* of the reference level; the predicate
|
|
176
|
+
// is declared in percent, which is what an operator writes down.
|
|
177
|
+
const pct = mover.deltaPct == null ? null : mover.deltaPct * 100;
|
|
178
|
+
if (pct == null) {
|
|
179
|
+
return quiet(`no comparable previous window for ${metric.id}`, window, bucket, current, {
|
|
180
|
+
value: mover.current,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const wanted = sub.predicate.direction ?? "any";
|
|
184
|
+
const directionOk = wanted === "any" ? true : wanted === "up" ? pct > 0 : pct < 0;
|
|
185
|
+
const fired = directionOk && Math.abs(pct) >= sub.predicate.pct;
|
|
186
|
+
return {
|
|
187
|
+
fired,
|
|
188
|
+
reason: fired
|
|
189
|
+
? `${metric.id} moved ${round(pct)}% versus the previous ${sub.evaluate.window} (threshold ±${sub.predicate.pct}%)`
|
|
190
|
+
: `${metric.id} moved ${round(pct)}%, inside the ±${sub.predicate.pct}% threshold`,
|
|
191
|
+
window,
|
|
192
|
+
value: mover.current,
|
|
193
|
+
expected: mover.previous,
|
|
194
|
+
sampleSize,
|
|
195
|
+
dimensionValue: null,
|
|
196
|
+
series: current,
|
|
197
|
+
bucket,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
case "anomaly": {
|
|
201
|
+
// Anomaly detection needs trailing history to know what "normal" was, so
|
|
202
|
+
// the read extends back over the primitive's own trailing window. Only
|
|
203
|
+
// buckets inside the *evaluation* window can fire — the history is context,
|
|
204
|
+
// not findings, or every restart would re-alert on last week.
|
|
205
|
+
const trailing = ANOMALY_TRAILING_BUCKETS[bucket];
|
|
206
|
+
const width = BUCKET_SECONDS[bucket] * 1000;
|
|
207
|
+
const historySince = floorToBucket(window.since - trailing * width, bucket);
|
|
208
|
+
const rows = await read({ since: historySince });
|
|
209
|
+
if (rows.length < ANOMALY_MIN_TRAILING + 1) {
|
|
210
|
+
return quiet(`only ${rows.length} bucket(s) of history; anomalies need more than ${ANOMALY_MIN_TRAILING}`, window, bucket, rows.filter((row) => inWindow(row.bucket, window)));
|
|
211
|
+
}
|
|
212
|
+
const anomalies = detectAnomalies(metric.id, scene, rows, {
|
|
213
|
+
bucket,
|
|
214
|
+
sensitivity: sub.predicate.sensitivity,
|
|
215
|
+
}).filter((row) => inWindow(row.bucketStart, window));
|
|
216
|
+
const inside = rows.filter((row) => inWindow(row.bucket, window));
|
|
217
|
+
if (anomalies.length === 0) {
|
|
218
|
+
return quiet(`no anomalous ${bucket} bucket for ${metric.id}`, window, bucket, inside);
|
|
219
|
+
}
|
|
220
|
+
// The most extreme finding inside the window is the one worth waking
|
|
221
|
+
// someone for; the rest are in the same episode.
|
|
222
|
+
const worst = anomalies.reduce((a, b) => (Math.abs(b.z ?? 0) > Math.abs(a.z ?? 0) ? b : a));
|
|
223
|
+
return {
|
|
224
|
+
fired: true,
|
|
225
|
+
reason: `${metric.id} was ${worst.kind} at ${new Date(worst.bucketStart).toISOString()}: ` +
|
|
226
|
+
`${round(worst.value)} against an expected ${round(worst.expected)} (robust z ${round(worst.z)})`,
|
|
227
|
+
window,
|
|
228
|
+
value: worst.value,
|
|
229
|
+
expected: worst.expected,
|
|
230
|
+
sampleSize: sampleOf(inside),
|
|
231
|
+
dimensionValue: worst.contributor?.value ?? null,
|
|
232
|
+
series: inside,
|
|
233
|
+
bucket,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
case "new_value": {
|
|
237
|
+
// Same spanning-scan trick as `movers`, grouped by the watched dimension:
|
|
238
|
+
// a value present in the current window and absent from the one before it
|
|
239
|
+
// is new. Bounded by construction — one grouped read, no retained state.
|
|
240
|
+
const reference = { since: window.since - windowMs, until: window.since };
|
|
241
|
+
const rows = await read({ since: reference.since, groupBy: sub.predicate.dimension });
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
for (const row of rows) {
|
|
244
|
+
if (inWindow(row.bucket, reference) && row.dimension_value != null) {
|
|
245
|
+
seen.add(row.dimension_value);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const fresh = [];
|
|
249
|
+
for (const row of rows) {
|
|
250
|
+
if (!inWindow(row.bucket, window))
|
|
251
|
+
continue;
|
|
252
|
+
const value = row.dimension_value;
|
|
253
|
+
// `''` is the store's "unattributed", not a value anyone named — firing
|
|
254
|
+
// on it would report "a new scene appeared" for every unlabelled event.
|
|
255
|
+
if (value == null || value === "" || seen.has(value))
|
|
256
|
+
continue;
|
|
257
|
+
if (!fresh.includes(value))
|
|
258
|
+
fresh.push(value);
|
|
259
|
+
}
|
|
260
|
+
const inside = rows.filter((row) => inWindow(row.bucket, window));
|
|
261
|
+
if (fresh.length === 0) {
|
|
262
|
+
return quiet(`no unseen ${sub.predicate.dimension} value in the window`, window, bucket, inside);
|
|
263
|
+
}
|
|
264
|
+
const first = fresh[0];
|
|
265
|
+
return {
|
|
266
|
+
fired: true,
|
|
267
|
+
reason: `new ${sub.predicate.dimension} ${JSON.stringify(first)}` +
|
|
268
|
+
(fresh.length > 1 ? ` (and ${fresh.length - 1} more)` : "") +
|
|
269
|
+
` on ${metric.id}`,
|
|
270
|
+
window,
|
|
271
|
+
value: fresh.length,
|
|
272
|
+
expected: seen.size,
|
|
273
|
+
sampleSize: sampleOf(inside),
|
|
274
|
+
dimensionValue: first,
|
|
275
|
+
series: inside,
|
|
276
|
+
bucket,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** Shape an evaluation into the firing record stored and delivered. */
|
|
282
|
+
export function toFiring(sub, result, at) {
|
|
283
|
+
return {
|
|
284
|
+
subscriptionId: sub.id,
|
|
285
|
+
name: sub.name,
|
|
286
|
+
metric: sub.metric,
|
|
287
|
+
predicate: sub.predicate.kind,
|
|
288
|
+
at,
|
|
289
|
+
window: { since: result.window.since, until: result.window.until },
|
|
290
|
+
value: result.value,
|
|
291
|
+
expected: result.expected,
|
|
292
|
+
sampleSize: result.sampleSize,
|
|
293
|
+
scene: sub.filters.scene ?? null,
|
|
294
|
+
reason: result.reason,
|
|
295
|
+
dimensionValue: result.dimensionValue,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* The bounded `format=summary` block a webhook carries alongside the firing
|
|
300
|
+
* (sketch §F.3: "so the receiver can act without a second call").
|
|
301
|
+
*
|
|
302
|
+
* It is the registry's own summary of `insight_baseline` over exactly the window
|
|
303
|
+
* that fired — a real registry metric with real declared units and caveats,
|
|
304
|
+
* rather than a hand-rolled bag of numbers. That is deliberate: the receiver
|
|
305
|
+
* reads the same envelope it would get from `GET /api/v1/insights/baseline`, so
|
|
306
|
+
* anything that can already render one can render this.
|
|
307
|
+
*
|
|
308
|
+
* `null` when the predicate read no series (`presence`) or the summariser
|
|
309
|
+
* declines the rows.
|
|
310
|
+
*/
|
|
311
|
+
export function summaryFor(sub, result) {
|
|
312
|
+
if (result.series.length === 0)
|
|
313
|
+
return null;
|
|
314
|
+
const baseline = computeBaseline(sub.metric, sub.filters.scene, result.series);
|
|
315
|
+
const summary = summarizeRows("insight_baseline", [baseline], {
|
|
316
|
+
range: { since: result.window.since, until: result.window.until },
|
|
317
|
+
filters: {
|
|
318
|
+
metric: sub.metric,
|
|
319
|
+
scene: sub.filters.scene,
|
|
320
|
+
bucket: result.bucket,
|
|
321
|
+
window: sub.evaluate.window,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
return summary == null ? null : summary;
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=evaluate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluate.js","sourceRoot":"","sources":["../../src/subscriptions/evaluate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { SubscriptionRecord } from "@uptimizr/db";
|
|
2
|
+
import type { CollectorConfig } from "../config.js";
|
|
3
|
+
import type { LiveBus } from "../liveBus.js";
|
|
4
|
+
import type { CollectorStore } from "../store.js";
|
|
5
|
+
import { type EvaluationResult } from "./evaluate.js";
|
|
6
|
+
import type { SubscriptionStream } from "./stream.js";
|
|
7
|
+
/**
|
|
8
|
+
* **The subscription scheduler** (#311, ADR 0051 §6 / sketch §F.2).
|
|
9
|
+
*
|
|
10
|
+
* One in-process scheduler for the whole collector. Its job is small and its
|
|
11
|
+
* constraints are the interesting part:
|
|
12
|
+
*
|
|
13
|
+
* - **Every subscription owns one timer**, firing on its own `evaluate.every`.
|
|
14
|
+
* Even the bus-backed predicates have one: `presence <= 0` ("nobody is in the
|
|
15
|
+
* scene") can only become true when events *stop* arriving, so a scheduler
|
|
16
|
+
* that only woke on bus traffic could never report it.
|
|
17
|
+
* - **The bus is an accelerator, not the clock.** `presence` and `new_value`
|
|
18
|
+
* additionally watch `LiveBus.subscribe` so they react in seconds rather than
|
|
19
|
+
* at the next tick. The state each one keeps is bounded
|
|
20
|
+
* ({@link NEW_VALUE_MAX_TRIGGERS}) and the trigger is rate-limited
|
|
21
|
+
* ({@link BUS_MIN_INTERVAL_MS}), so a firehose cannot turn into a query storm.
|
|
22
|
+
* The *answer* still comes from the store evaluation, so the bus can only
|
|
23
|
+
* change when a subscription is checked, never what it concludes.
|
|
24
|
+
* - **At most {@link CollectorConfig.subscriptionsMaxConcurrent} evaluations run
|
|
25
|
+
* at once**, and at most one per subscription. A tick that arrives while the
|
|
26
|
+
* previous one is still in flight is dropped, not queued — a subscription that
|
|
27
|
+
* cannot keep up with its own interval must not grow a backlog.
|
|
28
|
+
* - **Cooldown is enforced before delivery, not before evaluation.** The
|
|
29
|
+
* evaluation is cheap and its result is what `POST …/test` and the dashboard
|
|
30
|
+
* read; the *firing* is what costs a webhook.
|
|
31
|
+
* - **Every timer is `unref`'d and cleared by {@link SubscriptionScheduler.stop}.**
|
|
32
|
+
* A test that starts the scheduler and closes the app leaves nothing behind.
|
|
33
|
+
*/
|
|
34
|
+
/** Smallest gap between two bus-triggered evaluations of one subscription. */
|
|
35
|
+
export declare const BUS_MIN_INTERVAL_MS = 5000;
|
|
36
|
+
/**
|
|
37
|
+
* Distinct dimension values one `new_value` watcher remembers having triggered
|
|
38
|
+
* on. Past this the set is cleared and the watcher falls back to its timer — the
|
|
39
|
+
* predicate is for low-cardinality dimensions ("a new scene", "a new custom
|
|
40
|
+
* event"), and an unbounded set on a hot bus is exactly the kind of state
|
|
41
|
+
* ADR 0032 §6 exists to prevent.
|
|
42
|
+
*/
|
|
43
|
+
export declare const NEW_VALUE_MAX_TRIGGERS = 256;
|
|
44
|
+
/** How often the scheduler re-reads the store to pick up external changes. */
|
|
45
|
+
export declare const RELOAD_INTERVAL_MS = 60000;
|
|
46
|
+
export interface SchedulerDeps {
|
|
47
|
+
store: CollectorStore;
|
|
48
|
+
config: CollectorConfig;
|
|
49
|
+
liveBus: LiveBus;
|
|
50
|
+
stream: SubscriptionStream;
|
|
51
|
+
/** Structured logger; a Fastify instance's `log` in production. */
|
|
52
|
+
log: {
|
|
53
|
+
warn: (obj: unknown, msg?: string) => void;
|
|
54
|
+
info: (obj: unknown, msg?: string) => void;
|
|
55
|
+
};
|
|
56
|
+
/** Injected for tests. Defaults to the global `fetch`. */
|
|
57
|
+
fetchImpl?: typeof fetch;
|
|
58
|
+
/** Injected for tests. Defaults to `Date.now`. */
|
|
59
|
+
now?: () => number;
|
|
60
|
+
/** Injected for tests, so webhook backoff does not sleep for real. */
|
|
61
|
+
sleep?: (ms: number) => Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Fraction in `[0, 1)` used to jitter a subscription's first evaluation.
|
|
64
|
+
* Defaults to `Math.random`; tests pass `() => 0` for a deterministic phase.
|
|
65
|
+
*/
|
|
66
|
+
jitter?: () => number;
|
|
67
|
+
}
|
|
68
|
+
export interface RunOptions {
|
|
69
|
+
/** Deliver a firing (record it, fan it out, POST it). `false` for a dry run. */
|
|
70
|
+
deliver?: boolean;
|
|
71
|
+
}
|
|
72
|
+
export interface SubscriptionScheduler {
|
|
73
|
+
/** Reconcile timers and bus watchers against the store. Idempotent. */
|
|
74
|
+
reload(): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Evaluate one subscription now, outside its schedule. Returns the evaluation
|
|
77
|
+
* so `POST …/test` can answer with it. Honours cooldown when delivering.
|
|
78
|
+
*
|
|
79
|
+
* `null` means "not now": the subscription already had an evaluation in
|
|
80
|
+
* flight, or the collector was at its concurrency cap. The route turns that
|
|
81
|
+
* into a `409` rather than quietly running a second one in parallel.
|
|
82
|
+
*/
|
|
83
|
+
runOnce(sub: SubscriptionRecord, options?: RunOptions): Promise<EvaluationResult | null>;
|
|
84
|
+
/** Subscriptions currently scheduled. */
|
|
85
|
+
readonly scheduledCount: number;
|
|
86
|
+
/** Evaluations in flight (for tests asserting the concurrency cap). */
|
|
87
|
+
readonly inFlight: number;
|
|
88
|
+
/** Clear every timer and bus watcher. Idempotent. */
|
|
89
|
+
stop(): void;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Create the scheduler. It does nothing until {@link SubscriptionScheduler.reload}
|
|
93
|
+
* is called, which `app.ts` does once at startup and every CRUD write does after
|
|
94
|
+
* it changes something.
|
|
95
|
+
*/
|
|
96
|
+
export declare function createSubscriptionScheduler(deps: SchedulerDeps): SubscriptionScheduler;
|
|
97
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/subscriptions/scheduler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAkB,MAAM,eAAe,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAA8C,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAElG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAUzC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,mEAAmE;IACnE,GAAG,EAAE;QAAE,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,CAAC;IAChG,0DAA0D;IAC1D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,sEAAsE;IACtE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,gFAAgF;IAChF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,uEAAuE;IACvE,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB;;;;;;;OAOG;IACH,OAAO,CAAC,GAAG,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IACzF,yCAAyC;IACzC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,IAAI,IAAI,IAAI,CAAC;CACd;AA+BD;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,aAAa,GAAG,qBAAqB,CAgRtF"}
|