@firebreak/vitals 1.2.3 → 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 +25 -1
- package/dist/checks/databricks.d.cts +1 -1
- package/dist/checks/databricks.d.ts +1 -1
- package/dist/checks/http.d.cts +1 -1
- package/dist/checks/http.d.ts +1 -1
- package/dist/checks/postgres.d.cts +1 -1
- package/dist/checks/postgres.d.ts +1 -1
- package/dist/checks/redis.d.cts +1 -1
- package/dist/checks/redis.d.ts +1 -1
- package/dist/{core-BJ2Z0rRi.d.cts → core-Bee03bJm.d.cts} +7 -1
- package/dist/{core-BJ2Z0rRi.d.ts → core-Bee03bJm.d.ts} +7 -1
- package/dist/handler-BvjN4Ot9.d.ts +20 -0
- package/dist/handler-TZOgZvY7.d.cts +20 -0
- package/dist/index.cjs +286 -29
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.mjs +285 -29
- package/dist/integrations/express.cjs +268 -52
- package/dist/integrations/express.d.cts +2 -2
- package/dist/integrations/express.d.ts +2 -2
- package/dist/integrations/express.mjs +268 -52
- package/dist/integrations/next.cjs +294 -51
- package/dist/integrations/next.d.cts +3 -3
- package/dist/integrations/next.d.ts +3 -3
- package/dist/integrations/next.mjs +294 -51
- package/package.json +1 -1
- package/dist/handler-Bvf66wzh.d.cts +0 -26
- package/dist/handler-R2U3ygLo.d.ts +0 -26
package/dist/index.mjs
CHANGED
|
@@ -23,7 +23,7 @@ function statusFromString(value) {
|
|
|
23
23
|
return mapping[value.toLowerCase()] ?? Status.OUTAGE;
|
|
24
24
|
}
|
|
25
25
|
function toJson(response) {
|
|
26
|
-
|
|
26
|
+
const json = {
|
|
27
27
|
status: statusToLabel(response.status),
|
|
28
28
|
timestamp: response.timestamp,
|
|
29
29
|
checks: Object.fromEntries(
|
|
@@ -33,6 +33,10 @@ function toJson(response) {
|
|
|
33
33
|
])
|
|
34
34
|
)
|
|
35
35
|
};
|
|
36
|
+
if (response.cachedAt !== void 0) {
|
|
37
|
+
json.cachedAt = response.cachedAt;
|
|
38
|
+
}
|
|
39
|
+
return json;
|
|
36
40
|
}
|
|
37
41
|
function httpStatusCode(status) {
|
|
38
42
|
return status === Status.HEALTHY ? 200 : 503;
|
|
@@ -43,8 +47,11 @@ import { performance } from "perf_hooks";
|
|
|
43
47
|
var HealthcheckRegistry = class {
|
|
44
48
|
checks = [];
|
|
45
49
|
defaultTimeout;
|
|
50
|
+
cacheTtlMs;
|
|
51
|
+
cache = null;
|
|
46
52
|
constructor(options) {
|
|
47
53
|
this.defaultTimeout = options?.defaultTimeout ?? 5e3;
|
|
54
|
+
this.cacheTtlMs = options?.cacheTtlMs ?? 0;
|
|
48
55
|
}
|
|
49
56
|
add(name, fn, options) {
|
|
50
57
|
if (this.checks.some((c) => c.name === name)) {
|
|
@@ -69,6 +76,27 @@ var HealthcheckRegistry = class {
|
|
|
69
76
|
};
|
|
70
77
|
}
|
|
71
78
|
async run() {
|
|
79
|
+
if (this.cacheTtlMs <= 0) {
|
|
80
|
+
return this.executeChecks();
|
|
81
|
+
}
|
|
82
|
+
if (this.cache !== null && Date.now() < this.cache.expiresAt) {
|
|
83
|
+
return {
|
|
84
|
+
status: this.cache.status,
|
|
85
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
86
|
+
checks: structuredClone(this.cache.checks),
|
|
87
|
+
cachedAt: this.cache.cachedAt
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const response = await this.executeChecks();
|
|
91
|
+
this.cache = {
|
|
92
|
+
checks: structuredClone(response.checks),
|
|
93
|
+
status: response.status,
|
|
94
|
+
cachedAt: response.timestamp,
|
|
95
|
+
expiresAt: Date.now() + this.cacheTtlMs
|
|
96
|
+
};
|
|
97
|
+
return response;
|
|
98
|
+
}
|
|
99
|
+
async executeChecks() {
|
|
72
100
|
if (this.checks.length === 0) {
|
|
73
101
|
return {
|
|
74
102
|
status: Status.HEALTHY,
|
|
@@ -154,53 +182,281 @@ function extractToken(options) {
|
|
|
154
182
|
}
|
|
155
183
|
|
|
156
184
|
// src/handler.ts
|
|
157
|
-
var RESERVED_METADATA_KEYS = ["status", "timestamp", "checks"];
|
|
185
|
+
var RESERVED_METADATA_KEYS = ["status", "timestamp", "checks", "cachedAt"];
|
|
158
186
|
function createHealthcheckHandler(options) {
|
|
159
|
-
const { registry,
|
|
160
|
-
const token = rawToken || null;
|
|
187
|
+
const { registry, resolveDepth, metadata = {} } = options;
|
|
161
188
|
for (const key of Object.keys(metadata)) {
|
|
162
189
|
if (RESERVED_METADATA_KEYS.includes(key)) {
|
|
163
190
|
throw new Error(`Metadata key '${key}' is reserved. Use a different key name.`);
|
|
164
191
|
}
|
|
165
192
|
}
|
|
166
193
|
return async (req) => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
194
|
+
let depth = "shallow";
|
|
195
|
+
if (resolveDepth) {
|
|
196
|
+
try {
|
|
197
|
+
const result = await resolveDepth(req);
|
|
198
|
+
if (result === "deep" || result === "shallow") {
|
|
199
|
+
depth = result;
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
174
203
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (provided === null) {
|
|
181
|
-
const body = {
|
|
182
|
-
status: "ok",
|
|
183
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
184
|
-
...metadata
|
|
204
|
+
if (depth === "deep") {
|
|
205
|
+
const response = await registry.run();
|
|
206
|
+
return {
|
|
207
|
+
status: httpStatusCode(response.status),
|
|
208
|
+
body: { ...metadata, ...toJson(response) }
|
|
185
209
|
};
|
|
186
|
-
return { status: 200, body };
|
|
187
210
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
status: httpStatusCode(response.status),
|
|
194
|
-
body: { ...metadata, ...toJson(response) }
|
|
211
|
+
const body = {
|
|
212
|
+
status: "ok",
|
|
213
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
214
|
+
...metadata
|
|
195
215
|
};
|
|
216
|
+
return { status: 200, body };
|
|
196
217
|
};
|
|
197
218
|
}
|
|
219
|
+
|
|
220
|
+
// src/html.ts
|
|
221
|
+
function escapeHtml(str) {
|
|
222
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
223
|
+
}
|
|
224
|
+
function statusColor(status) {
|
|
225
|
+
switch (status) {
|
|
226
|
+
case "healthy":
|
|
227
|
+
case "ok":
|
|
228
|
+
return "#22C55E";
|
|
229
|
+
case "degraded":
|
|
230
|
+
return "#F59E0B";
|
|
231
|
+
case "outage":
|
|
232
|
+
return "#ED1C24";
|
|
233
|
+
default:
|
|
234
|
+
return "#ED1C24";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
var DEEP_KNOWN_KEYS = /* @__PURE__ */ new Set(["status", "timestamp", "checks", "cachedAt"]);
|
|
238
|
+
function isDeepResponse(body) {
|
|
239
|
+
return "checks" in body && "status" in body && body.status !== "ok";
|
|
240
|
+
}
|
|
241
|
+
function isShallowResponse(body) {
|
|
242
|
+
return "status" in body && body.status === "ok";
|
|
243
|
+
}
|
|
244
|
+
function isErrorResponse(body) {
|
|
245
|
+
return "error" in body && !("status" in body);
|
|
246
|
+
}
|
|
247
|
+
function renderStatusBadge(label, color) {
|
|
248
|
+
return `<span style="
|
|
249
|
+
display: inline-flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
gap: 8px;
|
|
252
|
+
padding: 4px 12px;
|
|
253
|
+
border-radius: 9999px;
|
|
254
|
+
background: ${color}1A;
|
|
255
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
256
|
+
font-size: 12px;
|
|
257
|
+
font-weight: 600;
|
|
258
|
+
letter-spacing: 0.05em;
|
|
259
|
+
text-transform: uppercase;
|
|
260
|
+
color: ${color};
|
|
261
|
+
"><span style="
|
|
262
|
+
width: 8px;
|
|
263
|
+
height: 8px;
|
|
264
|
+
border-radius: 50%;
|
|
265
|
+
background: ${color};
|
|
266
|
+
box-shadow: 0 0 6px ${color}80;
|
|
267
|
+
"></span>${escapeHtml(label)}</span>`;
|
|
268
|
+
}
|
|
269
|
+
function renderMetadataItems(items) {
|
|
270
|
+
if (items.length === 0) return "";
|
|
271
|
+
return `<div style="
|
|
272
|
+
display: flex;
|
|
273
|
+
flex-wrap: wrap;
|
|
274
|
+
gap: 24px;
|
|
275
|
+
margin-top: 16px;
|
|
276
|
+
">${items.map(
|
|
277
|
+
([label, value]) => `<div>
|
|
278
|
+
<div style="
|
|
279
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
280
|
+
font-size: 10px;
|
|
281
|
+
font-weight: 600;
|
|
282
|
+
letter-spacing: 0.05em;
|
|
283
|
+
text-transform: uppercase;
|
|
284
|
+
color: #A1A1AA;
|
|
285
|
+
margin-bottom: 4px;
|
|
286
|
+
">${escapeHtml(label)}</div>
|
|
287
|
+
<div style="
|
|
288
|
+
font-family: 'JetBrains Mono', monospace;
|
|
289
|
+
font-size: 13px;
|
|
290
|
+
color: #E4E4E7;
|
|
291
|
+
">${escapeHtml(value)}</div>
|
|
292
|
+
</div>`
|
|
293
|
+
).join("")}</div>`;
|
|
294
|
+
}
|
|
295
|
+
function renderCheckRow(name, check) {
|
|
296
|
+
const color = statusColor(check.status);
|
|
297
|
+
return `<div style="
|
|
298
|
+
display: flex;
|
|
299
|
+
align-items: center;
|
|
300
|
+
gap: 12px;
|
|
301
|
+
padding: 12px 0;
|
|
302
|
+
border-top: 1px solid #27272A;
|
|
303
|
+
">
|
|
304
|
+
<span style="
|
|
305
|
+
width: 8px;
|
|
306
|
+
height: 8px;
|
|
307
|
+
border-radius: 50%;
|
|
308
|
+
background: ${color};
|
|
309
|
+
box-shadow: 0 0 6px ${color}80;
|
|
310
|
+
flex-shrink: 0;
|
|
311
|
+
"></span>
|
|
312
|
+
<span style="
|
|
313
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
314
|
+
font-size: 12px;
|
|
315
|
+
text-transform: uppercase;
|
|
316
|
+
letter-spacing: 0.05em;
|
|
317
|
+
color: ${color};
|
|
318
|
+
font-weight: 600;
|
|
319
|
+
min-width: 72px;
|
|
320
|
+
">${escapeHtml(check.status)}</span>
|
|
321
|
+
<span style="
|
|
322
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
323
|
+
font-size: 14px;
|
|
324
|
+
font-weight: 500;
|
|
325
|
+
color: #E4E4E7;
|
|
326
|
+
flex: 1;
|
|
327
|
+
">${escapeHtml(name)}</span>
|
|
328
|
+
<span style="
|
|
329
|
+
font-family: 'JetBrains Mono', monospace;
|
|
330
|
+
font-size: 12px;
|
|
331
|
+
color: #A1A1AA;
|
|
332
|
+
min-width: 60px;
|
|
333
|
+
text-align: right;
|
|
334
|
+
">${escapeHtml(String(check.latencyMs))}ms</span>
|
|
335
|
+
<span style="
|
|
336
|
+
font-family: 'JetBrains Mono', monospace;
|
|
337
|
+
font-size: 12px;
|
|
338
|
+
color: #71717A;
|
|
339
|
+
max-width: 200px;
|
|
340
|
+
overflow: hidden;
|
|
341
|
+
text-overflow: ellipsis;
|
|
342
|
+
white-space: nowrap;
|
|
343
|
+
">${escapeHtml(check.message)}</span>
|
|
344
|
+
</div>`;
|
|
345
|
+
}
|
|
346
|
+
function renderPage(borderColor, content) {
|
|
347
|
+
return `<!DOCTYPE html>
|
|
348
|
+
<html lang="en">
|
|
349
|
+
<head>
|
|
350
|
+
<meta charset="utf-8">
|
|
351
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
352
|
+
<title>Healthcheck</title>
|
|
353
|
+
<style></style>
|
|
354
|
+
</head>
|
|
355
|
+
<body style="
|
|
356
|
+
margin: 0;
|
|
357
|
+
padding: 40px 20px;
|
|
358
|
+
background: #08080A;
|
|
359
|
+
min-height: 100vh;
|
|
360
|
+
display: flex;
|
|
361
|
+
justify-content: center;
|
|
362
|
+
align-items: flex-start;
|
|
363
|
+
box-sizing: border-box;
|
|
364
|
+
">
|
|
365
|
+
<div style="
|
|
366
|
+
width: 100%;
|
|
367
|
+
max-width: 560px;
|
|
368
|
+
background: #18181B;
|
|
369
|
+
border: 1px solid #27272A;
|
|
370
|
+
border-left: 3px solid ${borderColor};
|
|
371
|
+
border-radius: 8px;
|
|
372
|
+
padding: 24px;
|
|
373
|
+
">
|
|
374
|
+
${content}
|
|
375
|
+
</div>
|
|
376
|
+
</body>
|
|
377
|
+
</html>`;
|
|
378
|
+
}
|
|
379
|
+
function renderHealthcheckHtml(body) {
|
|
380
|
+
if (isErrorResponse(body)) {
|
|
381
|
+
const color = "#ED1C24";
|
|
382
|
+
return renderPage(
|
|
383
|
+
color,
|
|
384
|
+
`<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
385
|
+
<span style="
|
|
386
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
387
|
+
font-size: 18px;
|
|
388
|
+
font-weight: 700;
|
|
389
|
+
color: #E4E4E7;
|
|
390
|
+
">Healthcheck</span>
|
|
391
|
+
${renderStatusBadge("ERROR", color)}
|
|
392
|
+
</div>
|
|
393
|
+
<div style="
|
|
394
|
+
margin-top: 16px;
|
|
395
|
+
font-family: 'JetBrains Mono', monospace;
|
|
396
|
+
font-size: 13px;
|
|
397
|
+
color: #EF4444;
|
|
398
|
+
">${escapeHtml(body.error)}</div>`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
if (isShallowResponse(body)) {
|
|
402
|
+
const color = statusColor("ok");
|
|
403
|
+
const { status: _, timestamp, ...rest } = body;
|
|
404
|
+
const metadataItems = Object.entries(rest).map(
|
|
405
|
+
([k, v]) => [k, String(v)]
|
|
406
|
+
);
|
|
407
|
+
const allItems = [
|
|
408
|
+
["timestamp", timestamp],
|
|
409
|
+
...metadataItems
|
|
410
|
+
];
|
|
411
|
+
return renderPage(
|
|
412
|
+
color,
|
|
413
|
+
`<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
414
|
+
<span style="
|
|
415
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
416
|
+
font-size: 18px;
|
|
417
|
+
font-weight: 700;
|
|
418
|
+
color: #E4E4E7;
|
|
419
|
+
">Healthcheck</span>
|
|
420
|
+
${renderStatusBadge("OK", color)}
|
|
421
|
+
</div>
|
|
422
|
+
${renderMetadataItems(allItems)}`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
if (isDeepResponse(body)) {
|
|
426
|
+
const color = statusColor(body.status);
|
|
427
|
+
const { timestamp, checks } = body;
|
|
428
|
+
const metadataItems = Object.entries(body).filter(([k]) => !DEEP_KNOWN_KEYS.has(k)).map(([k, v]) => [k, String(v)]);
|
|
429
|
+
const allItems = [
|
|
430
|
+
["timestamp", timestamp],
|
|
431
|
+
...metadataItems
|
|
432
|
+
];
|
|
433
|
+
const checkRows = Object.entries(checks).sort(([a], [b]) => a.localeCompare(b)).map(([name, check]) => renderCheckRow(name, check)).join("");
|
|
434
|
+
return renderPage(
|
|
435
|
+
color,
|
|
436
|
+
`<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
437
|
+
<span style="
|
|
438
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
439
|
+
font-size: 18px;
|
|
440
|
+
font-weight: 700;
|
|
441
|
+
color: #E4E4E7;
|
|
442
|
+
">Healthcheck</span>
|
|
443
|
+
${renderStatusBadge(body.status, color)}
|
|
444
|
+
</div>
|
|
445
|
+
${renderMetadataItems(allItems)}
|
|
446
|
+
<div style="margin-top: 20px;">
|
|
447
|
+
${checkRows}
|
|
448
|
+
</div>`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return renderPage("#ED1C24", `<pre style="color: #E4E4E7; font-family: 'JetBrains Mono', monospace;">${escapeHtml(JSON.stringify(body, null, 2))}</pre>`);
|
|
452
|
+
}
|
|
198
453
|
export {
|
|
199
454
|
HealthcheckRegistry,
|
|
200
455
|
Status,
|
|
201
456
|
createHealthcheckHandler,
|
|
202
457
|
extractToken,
|
|
203
458
|
httpStatusCode,
|
|
459
|
+
renderHealthcheckHtml,
|
|
204
460
|
statusFromString,
|
|
205
461
|
statusToLabel,
|
|
206
462
|
syncCheck,
|