slotlock 0.3.0 → 0.5.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 +252 -0
- package/README.md +226 -17
- package/agent-server.d.ts +51 -8
- package/agent-server.js +478 -36
- package/agent-store-backend.d.ts +6 -0
- package/agent-store-backend.js +771 -30
- package/application-api.d.ts +22 -1
- package/application-api.js +412 -3
- package/booking-rules.d.ts +84 -0
- package/booking-rules.js +149 -0
- package/config.d.ts +20 -1
- package/config.js +33 -0
- package/dashboard.d.ts +61 -5
- package/dashboard.js +995 -139
- package/ddl.d.ts +5 -5
- package/ddl.js +590 -5
- package/engine.d.ts +5 -0
- package/engine.js +7 -4
- package/event-feed.d.ts +178 -0
- package/event-feed.js +435 -0
- package/index.d.ts +8 -2
- package/index.js +6 -1
- package/interop.d.ts +117 -0
- package/interop.js +356 -0
- package/maintenance.d.ts +14 -2
- package/maintenance.js +35 -3
- package/mcp-app.d.ts +7 -1
- package/mcp-app.js +259 -40
- package/metrics.d.ts +24 -0
- package/metrics.js +167 -0
- package/openapi.json +8346 -3400
- package/package.json +1 -1
- package/rate-limit.d.ts +10 -1
- package/rate-limit.js +25 -2
- package/rest-api.d.ts +22 -0
- package/rest-api.js +160 -15
- package/self-host.d.ts +26 -3
- package/self-host.js +316 -19
- package/slotlock-manifest.schema.json +1 -0
- package/slotlock.manifest.json +2 -1
- package/store.d.ts +317 -8
- package/store.js +1005 -89
- package/types.d.ts +87 -0
- package/types.js +0 -2
- package/webhooks.d.ts +91 -0
- package/webhooks.js +300 -0
package/metrics.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** Call duration buckets, in seconds. */
|
|
2
|
+
const BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10];
|
|
3
|
+
const REFUSAL_REASONS = [
|
|
4
|
+
'auth_refused',
|
|
5
|
+
'rate_limited',
|
|
6
|
+
'lookup_overflow',
|
|
7
|
+
'series_writes_busy',
|
|
8
|
+
'maintenance_busy',
|
|
9
|
+
'request_too_large',
|
|
10
|
+
];
|
|
11
|
+
const MAINTENANCE_COUNTS = [
|
|
12
|
+
'extended',
|
|
13
|
+
'conflicts',
|
|
14
|
+
'refused',
|
|
15
|
+
'pruned',
|
|
16
|
+
'zoneShifts',
|
|
17
|
+
'staleSources',
|
|
18
|
+
'noShows',
|
|
19
|
+
'waitlistOffers',
|
|
20
|
+
];
|
|
21
|
+
/** A label value: an operation, protocol or outcome code. Anything else is counted as `other`. */
|
|
22
|
+
const LABEL = /^[a-z0-9_.]{1,64}$/;
|
|
23
|
+
const label = (value) => (LABEL.test(value) ? value : 'other');
|
|
24
|
+
const key = (labels) => JSON.stringify(Object.entries(labels));
|
|
25
|
+
function formatLabels(labels) {
|
|
26
|
+
const entries = Object.entries(labels);
|
|
27
|
+
if (entries.length === 0)
|
|
28
|
+
return '';
|
|
29
|
+
// Label values are from LABEL's set or the version strings below: escaped anyway.
|
|
30
|
+
const escaped = entries.map(([name, value]) => `${name}="${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\n', '\\n')}"`);
|
|
31
|
+
return `{${escaped.join(',')}}`;
|
|
32
|
+
}
|
|
33
|
+
class Counter {
|
|
34
|
+
name;
|
|
35
|
+
help;
|
|
36
|
+
values = new Map();
|
|
37
|
+
constructor(name, help) {
|
|
38
|
+
this.name = name;
|
|
39
|
+
this.help = help;
|
|
40
|
+
}
|
|
41
|
+
add(labels, amount = 1) {
|
|
42
|
+
const id = key(labels);
|
|
43
|
+
const current = this.values.get(id) ?? { labels, value: 0 };
|
|
44
|
+
current.value += amount;
|
|
45
|
+
this.values.set(id, current);
|
|
46
|
+
}
|
|
47
|
+
render(type = 'counter') {
|
|
48
|
+
const lines = [`# HELP ${this.name} ${this.help}`, `# TYPE ${this.name} ${type}`];
|
|
49
|
+
for (const { labels, value } of this.values.values()) {
|
|
50
|
+
lines.push(`${this.name}${formatLabels(labels)} ${value}`);
|
|
51
|
+
}
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
class Histogram {
|
|
56
|
+
name;
|
|
57
|
+
help;
|
|
58
|
+
series = new Map();
|
|
59
|
+
constructor(name, help) {
|
|
60
|
+
this.name = name;
|
|
61
|
+
this.help = help;
|
|
62
|
+
}
|
|
63
|
+
observe(labels, seconds) {
|
|
64
|
+
const id = key(labels);
|
|
65
|
+
const current = this.series.get(id) ?? {
|
|
66
|
+
labels,
|
|
67
|
+
buckets: BUCKETS.map(() => 0),
|
|
68
|
+
sum: 0,
|
|
69
|
+
count: 0,
|
|
70
|
+
};
|
|
71
|
+
BUCKETS.forEach((bound, index) => {
|
|
72
|
+
if (seconds <= bound)
|
|
73
|
+
current.buckets[index] = (current.buckets[index] ?? 0) + 1;
|
|
74
|
+
});
|
|
75
|
+
current.sum += seconds;
|
|
76
|
+
current.count += 1;
|
|
77
|
+
this.series.set(id, current);
|
|
78
|
+
}
|
|
79
|
+
render() {
|
|
80
|
+
const lines = [`# HELP ${this.name} ${this.help}`, `# TYPE ${this.name} histogram`];
|
|
81
|
+
for (const { labels, buckets, sum, count } of this.series.values()) {
|
|
82
|
+
BUCKETS.forEach((bound, index) => {
|
|
83
|
+
lines.push(`${this.name}_bucket${formatLabels({ ...labels, le: String(bound) })} ${buckets[index]}`);
|
|
84
|
+
});
|
|
85
|
+
lines.push(`${this.name}_bucket${formatLabels({ ...labels, le: '+Inf' })} ${count}`);
|
|
86
|
+
lines.push(`${this.name}_sum${formatLabels(labels)} ${sum}`);
|
|
87
|
+
lines.push(`${this.name}_count${formatLabels(labels)} ${count}`);
|
|
88
|
+
}
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Counters for one server process, reset when it restarts (`slotlock_start_time_seconds`). */
|
|
93
|
+
export function createSlotlockMetrics(info) {
|
|
94
|
+
const started = (info.now ?? Date.now)() / 1000;
|
|
95
|
+
const calls = new Counter('slotlock_calls_total', 'Agent and REST calls by protocol, operation and outcome (ok, a refusal code, or an HTTP status).');
|
|
96
|
+
const duration = new Histogram('slotlock_call_duration_seconds', 'How long agent and REST calls took, by protocol and operation.');
|
|
97
|
+
const confirmations = new Counter('slotlock_confirmations_total', 'Write confirmations asked of a person, by operation and outcome.');
|
|
98
|
+
const subscriptions = new Counter('slotlock_subscriptions_total', 'MCP subscriptions opened and closed, by outcome and the reason one closed.');
|
|
99
|
+
let open = 0;
|
|
100
|
+
const refused = new Counter('slotlock_refusals_total', 'Requests refused before they ran, by reason.');
|
|
101
|
+
const passes = new Counter('slotlock_maintenance_passes_total', 'Maintenance passes, by whether one was capped.');
|
|
102
|
+
const upkeep = new Counter('slotlock_maintenance_items_total', 'What maintenance did, by kind: series extended, holds pruned, no-shows released, waitlist offers made, and so on.');
|
|
103
|
+
const deliveries = new Counter('slotlock_webhook_deliveries_total', 'Webhook changes delivered, and deliveries that failed and will be retried.');
|
|
104
|
+
return {
|
|
105
|
+
event(event) {
|
|
106
|
+
if (event.type === 'call' || event.type === 'rest') {
|
|
107
|
+
const protocol = event.type === 'rest' ? 'rest' : event.protocol;
|
|
108
|
+
const operation = label(event.operation);
|
|
109
|
+
const outcome = event.type === 'rest' ? String(event.status) : label(event.outcome);
|
|
110
|
+
calls.add({ protocol, operation, outcome });
|
|
111
|
+
duration.observe({ protocol, operation }, Math.max(0, event.durationMs) / 1000);
|
|
112
|
+
}
|
|
113
|
+
else if (event.type === 'confirmation') {
|
|
114
|
+
confirmations.add({ operation: label(event.operation), outcome: event.outcome });
|
|
115
|
+
}
|
|
116
|
+
else if (event.type === 'subscription') {
|
|
117
|
+
subscriptions.add({ outcome: event.outcome, reason: event.reason ?? 'none' });
|
|
118
|
+
open = Math.max(0, open + (event.outcome === 'opened' ? 1 : -1));
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
refusals(counts) {
|
|
122
|
+
for (const reason of REFUSAL_REASONS) {
|
|
123
|
+
const amount = counts[reason];
|
|
124
|
+
if (amount > 0)
|
|
125
|
+
refused.add({ reason }, amount);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
maintenance(result) {
|
|
129
|
+
passes.add({ capped: String(result.capped) });
|
|
130
|
+
for (const kind of MAINTENANCE_COUNTS) {
|
|
131
|
+
if (result[kind] > 0)
|
|
132
|
+
upkeep.add({ kind }, result[kind]);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
refusedScrape() {
|
|
136
|
+
refused.add({ reason: 'metrics_auth_refused' });
|
|
137
|
+
},
|
|
138
|
+
webhooks(result) {
|
|
139
|
+
if (result.delivered > 0)
|
|
140
|
+
deliveries.add({ result: 'delivered' }, result.delivered);
|
|
141
|
+
if (result.failed > 0)
|
|
142
|
+
deliveries.add({ result: 'failed' }, result.failed);
|
|
143
|
+
},
|
|
144
|
+
render() {
|
|
145
|
+
const lines = [
|
|
146
|
+
'# HELP slotlock_build_info The Slotlock version and tz database the server runs.',
|
|
147
|
+
'# TYPE slotlock_build_info gauge',
|
|
148
|
+
`slotlock_build_info${formatLabels({ version: info.version, tzdata: info.tzdata ?? 'unknown' })} 1`,
|
|
149
|
+
'# HELP slotlock_start_time_seconds When the server started, in seconds since the epoch.',
|
|
150
|
+
'# TYPE slotlock_start_time_seconds gauge',
|
|
151
|
+
`slotlock_start_time_seconds ${started}`,
|
|
152
|
+
'# HELP slotlock_subscriptions_open MCP subscriptions open now.',
|
|
153
|
+
'# TYPE slotlock_subscriptions_open gauge',
|
|
154
|
+
`slotlock_subscriptions_open ${open}`,
|
|
155
|
+
...calls.render(),
|
|
156
|
+
...duration.render(),
|
|
157
|
+
...confirmations.render(),
|
|
158
|
+
...subscriptions.render(),
|
|
159
|
+
...refused.render(),
|
|
160
|
+
...passes.render(),
|
|
161
|
+
...upkeep.render(),
|
|
162
|
+
...deliveries.render(),
|
|
163
|
+
];
|
|
164
|
+
return `${lines.join('\n')}\n`;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|