pipework 0.284.0 → 0.286.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/CHANGELOG.md +11 -0
- package/README.md +6 -0
- package/REFERENCE.md +6 -0
- package/dist/core/config/load.d.ts +1 -1
- package/dist/core/config/load.d.ts.map +1 -1
- package/dist/core/config/load.js +6 -1
- package/dist/core/config/load.js.map +1 -1
- package/dist/core/config/namespace.d.ts +1 -0
- package/dist/core/config/namespace.d.ts.map +1 -1
- package/dist/core/config/schema.d.ts +3 -1
- package/dist/core/config/schema.d.ts.map +1 -1
- package/dist/core/config/schema.js +4 -1
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/config/statement-observer.d.ts +45 -9
- package/dist/core/config/statement-observer.d.ts.map +1 -1
- package/dist/core/config/statement-observer.js +26 -1
- package/dist/core/config/statement-observer.js.map +1 -1
- package/dist/data/db/expressions.d.ts +10 -0
- package/dist/data/db/expressions.d.ts.map +1 -1
- package/dist/data/db/expressions.js +14 -0
- package/dist/data/db/expressions.js.map +1 -1
- package/dist/data/db/filter.d.ts +4 -2
- package/dist/data/db/filter.d.ts.map +1 -1
- package/dist/data/db/filter.js +4 -2
- package/dist/data/db/filter.js.map +1 -1
- package/dist/data/db/namespace.d.ts +3 -1
- package/dist/data/db/namespace.d.ts.map +1 -1
- package/dist/data/db/namespace.js +1 -1
- package/dist/data/db/namespace.js.map +1 -1
- package/dist/data/db/query-watch.d.ts.map +1 -1
- package/dist/data/db/query-watch.js +23 -6
- package/dist/data/db/query-watch.js.map +1 -1
- package/dist/data/jsonb/namespace.d.ts +3 -1
- package/dist/data/jsonb/namespace.d.ts.map +1 -1
- package/dist/data/jsonb/namespace.js +3 -1
- package/dist/data/jsonb/namespace.js.map +1 -1
- package/dist/data/jsonb/query.d.ts +8 -0
- package/dist/data/jsonb/query.d.ts.map +1 -1
- package/dist/data/jsonb/query.js +19 -3
- package/dist/data/jsonb/query.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/infra/alerts/index.d.ts +1 -0
- package/dist/infra/alerts/index.d.ts.map +1 -1
- package/dist/infra/alerts/namespace.d.ts +3 -0
- package/dist/infra/alerts/namespace.d.ts.map +1 -1
- package/dist/infra/alerts/namespace.js +3 -0
- package/dist/infra/alerts/namespace.js.map +1 -1
- package/dist/infra/alerts/statement-sources.d.ts +27 -0
- package/dist/infra/alerts/statement-sources.d.ts.map +1 -0
- package/dist/infra/alerts/statement-sources.js +84 -0
- package/dist/infra/alerts/statement-sources.js.map +1 -0
- package/package.json +1 -1
- package/src/core/config/load.ts +7 -2
- package/src/core/config/schema.ts +5 -2
- package/src/core/config/statement-observer.ts +65 -9
- package/src/data/db/expressions.ts +15 -0
- package/src/data/db/filter.ts +4 -2
- package/src/data/db/namespace.ts +1 -1
- package/src/data/db/query-watch.ts +25 -6
- package/src/data/jsonb/namespace.ts +3 -1
- package/src/data/jsonb/query.ts +22 -3
- package/src/index.ts +1 -1
- package/src/infra/alerts/index.ts +1 -0
- package/src/infra/alerts/namespace.ts +3 -0
- package/src/infra/alerts/statement-sources.ts +94 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// An alert source over settled statements: per `pipe.labelled` label, the p90 of its last N durations against a budget.
|
|
2
|
+
|
|
3
|
+
import type { StatementSettledEvent, StatementSettledFeed } from '../../core/config/statement-observer.js'
|
|
4
|
+
import type { AlertSeverity } from './handle.js'
|
|
5
|
+
import type { AlertCondition, AlertSource } from './sources.js'
|
|
6
|
+
|
|
7
|
+
export interface StatementTimingOptions {
|
|
8
|
+
/** Budget in ms per label: `{ 'gate-read': 1000 }`. A label not named here is never recorded. At least one. */
|
|
9
|
+
readonly budgets: Readonly<Record<string, number>>
|
|
10
|
+
/** N: statements per label the p90 is taken over. The source stays quiet for a label until N have settled. */
|
|
11
|
+
readonly window: number
|
|
12
|
+
/** Default `warning`, as the backlog source. */
|
|
13
|
+
readonly severity?: AlertSeverity
|
|
14
|
+
readonly owner?: string
|
|
15
|
+
readonly runbook?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** What `alerts.source.statementTiming` returns: an `AlertSource` that is also passed as `observability.statementTiming`. */
|
|
19
|
+
export type StatementTimingSource = AlertSource & StatementSettledFeed
|
|
20
|
+
|
|
21
|
+
interface Ring { readonly samples: number[]; next: number }
|
|
22
|
+
|
|
23
|
+
/** Nearest rank: the smallest sample that at least 90% of the samples are at or under. */
|
|
24
|
+
function p90(samples: readonly number[]): number {
|
|
25
|
+
const sorted = [...samples].sort((a, b) => a - b)
|
|
26
|
+
return sorted[Math.ceil(sorted.length * 0.9) - 1]!
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Raises `statement-slow:<label>` while the p90 duration of a label's last `window` settled statements is over
|
|
31
|
+
* that label's budget; resolves when it is back under. Statements are fed in-process by
|
|
32
|
+
* `observability.statementTiming` (pass the returned source there as well as to `alerts.create({ sources })`;
|
|
33
|
+
* the app's own `onStatementSettled` still fires). Only the labels in `budgets` are recorded, each in a ring of
|
|
34
|
+
* `window` numbers, so memory is bounded by `window` times the labels named; unlabelled statements and other
|
|
35
|
+
* labels are ignored. Failed statements count, with the time they took. The window is this process's: a tick sees
|
|
36
|
+
* only statements that settled in its own process, and a label that stops being called keeps its last window.
|
|
37
|
+
* Context carries `label`, `p90Ms`, `window` and `budgetMs`.
|
|
38
|
+
*/
|
|
39
|
+
export function statementTimingSource(options: StatementTimingOptions): StatementTimingSource {
|
|
40
|
+
const who = 'alerts.source.statementTiming'
|
|
41
|
+
if (!Number.isInteger(options.window) || options.window < 1) {
|
|
42
|
+
throw new Error(`[pipework] ${who} window must be a positive integer number of statements, got ${String(options.window)}.`)
|
|
43
|
+
}
|
|
44
|
+
const entries = Object.entries(options.budgets)
|
|
45
|
+
if (entries.length === 0) throw new Error(`[pipework] ${who} needs budgets: at least one label and its budget in ms.`)
|
|
46
|
+
const budgets = new Map<string, number>()
|
|
47
|
+
for (const [label, budget] of entries) {
|
|
48
|
+
if (label === '') throw new Error(`[pipework] ${who} budgets: a label must not be empty.`)
|
|
49
|
+
if (typeof budget !== 'number' || !Number.isFinite(budget) || budget <= 0) {
|
|
50
|
+
throw new Error(`[pipework] ${who} budget for "${label}" must be a positive number of milliseconds, got ${String(budget)}.`)
|
|
51
|
+
}
|
|
52
|
+
budgets.set(label, budget)
|
|
53
|
+
}
|
|
54
|
+
const size = options.window
|
|
55
|
+
const severity = options.severity ?? 'warning'
|
|
56
|
+
const rings = new Map<string, Ring>()
|
|
57
|
+
let attached = false
|
|
58
|
+
const prefix = 'statement-slow:'
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
name: 'statement-slow',
|
|
62
|
+
owns: key => key.startsWith(prefix),
|
|
63
|
+
observe(event: StatementSettledEvent): void {
|
|
64
|
+
if (event.label === null || !budgets.has(event.label)) return
|
|
65
|
+
let ring = rings.get(event.label)
|
|
66
|
+
if (ring === undefined) { ring = { samples: [], next: 0 }; rings.set(event.label, ring) }
|
|
67
|
+
if (ring.samples.length < size) ring.samples.push(event.durationMs)
|
|
68
|
+
else { ring.samples[ring.next] = event.durationMs; ring.next = (ring.next + 1) % size }
|
|
69
|
+
},
|
|
70
|
+
attach(): void { attached = true },
|
|
71
|
+
evaluate(): Promise<AlertCondition[]> {
|
|
72
|
+
if (!attached) {
|
|
73
|
+
return Promise.reject(new Error(`[pipework] ${who} was never given to the blueprint: set observability.statementTiming to this source, or it sees no statements.`))
|
|
74
|
+
}
|
|
75
|
+
const out: AlertCondition[] = []
|
|
76
|
+
for (const [label, budget] of budgets) {
|
|
77
|
+
const ring = rings.get(label)
|
|
78
|
+
if (ring === undefined || ring.samples.length < size) continue
|
|
79
|
+
const value = p90(ring.samples)
|
|
80
|
+
if (value <= budget) continue
|
|
81
|
+
out.push({
|
|
82
|
+
key: `${prefix}${label}`,
|
|
83
|
+
severity,
|
|
84
|
+
message: `p90 of the last ${String(size)} "${label}" statements is ${String(value)} ms, over its ${String(budget)} ms budget`,
|
|
85
|
+
subject: { entity: 'statement-label', entityId: label },
|
|
86
|
+
...(options.owner !== undefined ? { owner: options.owner } : {}),
|
|
87
|
+
...(options.runbook !== undefined ? { runbook: options.runbook } : {}),
|
|
88
|
+
context: { label, p90Ms: value, window: size, budgetMs: budget },
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
return Promise.resolve(out)
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|