@growth-labs/monitoring 0.1.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 +9 -0
- package/README.md +115 -0
- package/SPEC.md +19 -0
- package/dist/alerting/dedup.d.ts +6 -0
- package/dist/alerting/dedup.d.ts.map +1 -0
- package/dist/alerting/dedup.js +49 -0
- package/dist/alerting/dedup.js.map +1 -0
- package/dist/alerting/escalation.d.ts +4 -0
- package/dist/alerting/escalation.d.ts.map +1 -0
- package/dist/alerting/escalation.js +26 -0
- package/dist/alerting/escalation.js.map +1 -0
- package/dist/alerting/index.d.ts +31 -0
- package/dist/alerting/index.d.ts.map +1 -0
- package/dist/alerting/index.js +50 -0
- package/dist/alerting/index.js.map +1 -0
- package/dist/alerting/thresholds.d.ts +8 -0
- package/dist/alerting/thresholds.d.ts.map +1 -0
- package/dist/alerting/thresholds.js +105 -0
- package/dist/alerting/thresholds.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/prober/index.d.ts +19 -0
- package/dist/prober/index.d.ts.map +1 -0
- package/dist/prober/index.js +48 -0
- package/dist/prober/index.js.map +1 -0
- package/dist/prober/persist.d.ts +4 -0
- package/dist/prober/persist.d.ts.map +1 -0
- package/dist/prober/persist.js +21 -0
- package/dist/prober/persist.js.map +1 -0
- package/dist/prober/runners/get-runner.d.ts +4 -0
- package/dist/prober/runners/get-runner.d.ts.map +1 -0
- package/dist/prober/runners/get-runner.js +43 -0
- package/dist/prober/runners/get-runner.js.map +1 -0
- package/dist/prober/runners/happy-path-runner.d.ts +13 -0
- package/dist/prober/runners/happy-path-runner.d.ts.map +1 -0
- package/dist/prober/runners/happy-path-runner.js +183 -0
- package/dist/prober/runners/happy-path-runner.js.map +1 -0
- package/dist/prober/runners/post-runner.d.ts +4 -0
- package/dist/prober/runners/post-runner.d.ts.map +1 -0
- package/dist/prober/runners/post-runner.js +44 -0
- package/dist/prober/runners/post-runner.js.map +1 -0
- package/dist/prober/surfaces.d.ts +46 -0
- package/dist/prober/surfaces.d.ts.map +1 -0
- package/dist/prober/surfaces.js +2 -0
- package/dist/prober/surfaces.js.map +1 -0
- package/dist/schemas/drizzle/schema.d.ts +519 -0
- package/dist/schemas/drizzle/schema.d.ts.map +1 -0
- package/dist/schemas/drizzle/schema.js +45 -0
- package/dist/schemas/drizzle/schema.js.map +1 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +2 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/migrations/0001_uptime_checks.sql +12 -0
- package/dist/schemas/migrations/0002_uptime_incidents.sql +12 -0
- package/dist/schemas/migrations/0003_errors.sql +16 -0
- package/dist/schemas/migrations/README.md +15 -0
- package/dist/status-page/app.d.ts +10 -0
- package/dist/status-page/app.d.ts.map +1 -0
- package/dist/status-page/app.js +44 -0
- package/dist/status-page/app.js.map +1 -0
- package/dist/status-page/lib/queries.d.ts +6 -0
- package/dist/status-page/lib/queries.d.ts.map +1 -0
- package/dist/status-page/lib/queries.js +81 -0
- package/dist/status-page/lib/queries.js.map +1 -0
- package/dist/status-page/pages/api/status.json.d.ts +3 -0
- package/dist/status-page/pages/api/status.json.d.ts.map +1 -0
- package/dist/status-page/pages/api/status.json.js +19 -0
- package/dist/status-page/pages/api/status.json.js.map +1 -0
- package/dist/status-page/shell.d.ts +3 -0
- package/dist/status-page/shell.d.ts.map +1 -0
- package/dist/status-page/shell.js +18 -0
- package/dist/status-page/shell.js.map +1 -0
- package/dist/tail/categorize.d.ts +44 -0
- package/dist/tail/categorize.d.ts.map +1 -0
- package/dist/tail/categorize.js +113 -0
- package/dist/tail/categorize.js.map +1 -0
- package/dist/tail/fingerprint.d.ts +4 -0
- package/dist/tail/fingerprint.d.ts.map +1 -0
- package/dist/tail/fingerprint.js +18 -0
- package/dist/tail/fingerprint.js.map +1 -0
- package/dist/tail/index.d.ts +21 -0
- package/dist/tail/index.d.ts.map +1 -0
- package/dist/tail/index.js +50 -0
- package/dist/tail/index.js.map +1 -0
- package/dist/tail/persist.d.ts +15 -0
- package/dist/tail/persist.d.ts.map +1 -0
- package/dist/tail/persist.js +63 -0
- package/dist/tail/persist.js.map +1 -0
- package/dist/tail/redact.d.ts +5 -0
- package/dist/tail/redact.d.ts.map +1 -0
- package/dist/tail/redact.js +25 -0
- package/dist/tail/redact.js.map +1 -0
- package/dist/tail/sample.d.ts +9 -0
- package/dist/tail/sample.d.ts.map +1 -0
- package/dist/tail/sample.js +25 -0
- package/dist/tail/sample.js.map +1 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/package.json +85 -0
- package/src/schemas/migrations/0001_uptime_checks.sql +12 -0
- package/src/schemas/migrations/0002_uptime_incidents.sql +12 -0
- package/src/schemas/migrations/0003_errors.sql +16 -0
- package/src/schemas/migrations/README.md +15 -0
- package/src/status-page/README.md +14 -0
- package/src/status-page/app.ts +58 -0
- package/src/status-page/components/ErrorRollup.astro +26 -0
- package/src/status-page/components/IncidentList.astro +25 -0
- package/src/status-page/components/SurfaceRow.astro +24 -0
- package/src/status-page/lib/queries.ts +114 -0
- package/src/status-page/pages/api/status.json.ts +24 -0
- package/src/status-page/pages/index.astro +45 -0
- package/src/status-page/shell.ts +17 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SamplingConfig } from '../types.js';
|
|
2
|
+
import type { ErrorCategory } from './categorize.js';
|
|
3
|
+
interface SamplingOptions {
|
|
4
|
+
surface?: string;
|
|
5
|
+
activeIncidentSurfaces?: Set<string>;
|
|
6
|
+
}
|
|
7
|
+
export declare function shouldKeep(category: ErrorCategory, config: SamplingConfig, options?: SamplingOptions): boolean;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=sample.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sample.d.ts","sourceRoot":"","sources":["../../src/tail/sample.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,UAAU,eAAe;IACxB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sBAAsB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CACpC;AAED,wBAAgB,UAAU,CACzB,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,eAAoB,GAC3B,OAAO,CAMT"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function shouldKeep(category, config, options = {}) {
|
|
2
|
+
if (options.surface && options.activeIncidentSurfaces?.has(options.surface))
|
|
3
|
+
return true;
|
|
4
|
+
const pct = pctForCategory(category, config);
|
|
5
|
+
if (pct >= 1)
|
|
6
|
+
return true;
|
|
7
|
+
if (pct <= 0)
|
|
8
|
+
return false;
|
|
9
|
+
return Math.random() < pct;
|
|
10
|
+
}
|
|
11
|
+
function pctForCategory(category, config) {
|
|
12
|
+
switch (category) {
|
|
13
|
+
case 'exception':
|
|
14
|
+
return config.exceptionsPct;
|
|
15
|
+
case 'fivexx':
|
|
16
|
+
return config.fivexxPct;
|
|
17
|
+
case 'console-error':
|
|
18
|
+
return config.consoleErrorPct;
|
|
19
|
+
case 'console-warn':
|
|
20
|
+
return config.consoleWarnPct;
|
|
21
|
+
case 'slow-request':
|
|
22
|
+
return config.slowRequestsPct;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=sample.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sample.js","sourceRoot":"","sources":["../../src/tail/sample.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,UAAU,CACzB,QAAuB,EACvB,MAAsB,EACtB,UAA2B,EAAE;IAE7B,IAAI,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,sBAAsB,EAAE,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAA;IACxF,MAAM,GAAG,GAAG,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IAC5C,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IACzB,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,KAAK,CAAA;IAC1B,OAAO,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAA;AAC3B,CAAC;AAED,SAAS,cAAc,CAAC,QAAuB,EAAE,MAAsB;IACtE,QAAQ,QAAQ,EAAE,CAAC;QAClB,KAAK,WAAW;YACf,OAAO,MAAM,CAAC,aAAa,CAAA;QAC5B,KAAK,QAAQ;YACZ,OAAO,MAAM,CAAC,SAAS,CAAA;QACxB,KAAK,eAAe;YACnB,OAAO,MAAM,CAAC,eAAe,CAAA;QAC9B,KAAK,cAAc;YAClB,OAAO,MAAM,CAAC,cAAc,CAAA;QAC7B,KAAK,cAAc;YAClB,OAAO,MAAM,CAAC,eAAe,CAAA;IAC/B,CAAC;AACF,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export type CheckType = 'get' | 'post' | 'happy_path';
|
|
2
|
+
export type CheckStatus = 'pass' | 'fail' | 'timeout';
|
|
3
|
+
export type AlertSeverity = 'info' | 'warning' | 'critical';
|
|
4
|
+
export type ErrorSeverity = 'error' | 'warning' | 'exception';
|
|
5
|
+
export interface CheckResult {
|
|
6
|
+
id?: string;
|
|
7
|
+
status: CheckStatus;
|
|
8
|
+
statusCode?: number;
|
|
9
|
+
latencyMs: number;
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
checkedAt?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface PersistedCheckResult extends CheckResult {
|
|
14
|
+
id: string;
|
|
15
|
+
checkedAt: number;
|
|
16
|
+
}
|
|
17
|
+
export interface NotifyConfig {
|
|
18
|
+
channels: Array<'slack' | 'email'>;
|
|
19
|
+
emailProvider?: 'cloudflare' | 'resend';
|
|
20
|
+
}
|
|
21
|
+
export interface ProbeAlertingConfig {
|
|
22
|
+
consecutiveFailuresToOpen?: number;
|
|
23
|
+
consecutiveSuccessesToClose?: number;
|
|
24
|
+
minSeverity?: 'warning' | 'critical';
|
|
25
|
+
}
|
|
26
|
+
export interface TailAlertingConfig {
|
|
27
|
+
newErrorDedupWindowMs?: number;
|
|
28
|
+
rateSpikeThreshold?: number;
|
|
29
|
+
rateSpikeWindowMs?: number;
|
|
30
|
+
surfaceDownThresholdPct?: number;
|
|
31
|
+
surfaceDownWindowMs?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface ThresholdConfig extends ProbeAlertingConfig, TailAlertingConfig {
|
|
34
|
+
}
|
|
35
|
+
export interface SamplingConfig {
|
|
36
|
+
exceptionsPct: number;
|
|
37
|
+
fivexxPct: number;
|
|
38
|
+
consoleErrorPct: number;
|
|
39
|
+
consoleWarnPct: number;
|
|
40
|
+
slowRequestsPct: number;
|
|
41
|
+
slowRequestThresholdMs: number;
|
|
42
|
+
}
|
|
43
|
+
export interface RuntimeFetch {
|
|
44
|
+
fetcher?: typeof fetch;
|
|
45
|
+
}
|
|
46
|
+
export interface UptimeIncident {
|
|
47
|
+
id: string;
|
|
48
|
+
surface: string;
|
|
49
|
+
opened_at: number;
|
|
50
|
+
closed_at: number | null;
|
|
51
|
+
trigger_check_id: string | null;
|
|
52
|
+
resolve_check_id: string | null;
|
|
53
|
+
severity: 'warning' | 'critical';
|
|
54
|
+
notes: string | null;
|
|
55
|
+
}
|
|
56
|
+
export interface SurfaceStatus {
|
|
57
|
+
name: string;
|
|
58
|
+
status: 'green' | 'yellow' | 'red';
|
|
59
|
+
lastCheckedAt: number | null;
|
|
60
|
+
uptime7d: number;
|
|
61
|
+
}
|
|
62
|
+
export interface ErrorRollup {
|
|
63
|
+
fingerprint: string;
|
|
64
|
+
surface: string;
|
|
65
|
+
message: string;
|
|
66
|
+
count: number;
|
|
67
|
+
lastOccurredAt: number;
|
|
68
|
+
}
|
|
69
|
+
export interface StatusPageSurfaceConfig {
|
|
70
|
+
name: string;
|
|
71
|
+
}
|
|
72
|
+
export interface StatusPageConfig {
|
|
73
|
+
realm: string;
|
|
74
|
+
d1Binding?: string;
|
|
75
|
+
surfaces: StatusPageSurfaceConfig[];
|
|
76
|
+
}
|
|
77
|
+
export interface ScheduledEventLike {
|
|
78
|
+
cron: string;
|
|
79
|
+
}
|
|
80
|
+
export interface ExecutionContextLike {
|
|
81
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
82
|
+
}
|
|
83
|
+
export type IdFactory = () => string;
|
|
84
|
+
export declare function generateId(): string;
|
|
85
|
+
export declare function errorMessage(error: unknown): string;
|
|
86
|
+
export declare function isTimeoutMessage(message: string): boolean;
|
|
87
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function generateId() {
|
|
2
|
+
return crypto.randomUUID();
|
|
3
|
+
}
|
|
4
|
+
export function errorMessage(error) {
|
|
5
|
+
return error instanceof Error ? error.message : String(error);
|
|
6
|
+
}
|
|
7
|
+
export function isTimeoutMessage(message) {
|
|
8
|
+
const lower = message.toLowerCase();
|
|
9
|
+
return lower.includes('timed out') || lower.includes('timeout') || lower.includes('aborted');
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAmGA,MAAM,UAAU,UAAU;IACzB,OAAO,MAAM,CAAC,UAAU,EAAE,CAAA;AAC3B,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAc;IAC1C,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC9D,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAA;IACnC,OAAO,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;AAC7F,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@growth-labs/monitoring",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Operational observability primitives for Growth Labs Cloudflare Workers: synthetic probes, Tail Worker capture, alerting, D1 schemas, and a status page.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./prober": {
|
|
13
|
+
"types": "./dist/prober/index.d.ts",
|
|
14
|
+
"import": "./dist/prober/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./tail": {
|
|
17
|
+
"types": "./dist/tail/index.d.ts",
|
|
18
|
+
"import": "./dist/tail/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./alerting": {
|
|
21
|
+
"types": "./dist/alerting/index.d.ts",
|
|
22
|
+
"import": "./dist/alerting/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./status-page": {
|
|
25
|
+
"types": "./dist/status-page/app.d.ts",
|
|
26
|
+
"import": "./dist/status-page/app.js"
|
|
27
|
+
},
|
|
28
|
+
"./status-page/queries": {
|
|
29
|
+
"types": "./dist/status-page/lib/queries.d.ts",
|
|
30
|
+
"import": "./dist/status-page/lib/queries.js"
|
|
31
|
+
},
|
|
32
|
+
"./status-page/components/*": {
|
|
33
|
+
"types": "./src/status-page/components/*.astro",
|
|
34
|
+
"import": "./src/status-page/components/*.astro"
|
|
35
|
+
},
|
|
36
|
+
"./status-page/pages/*": {
|
|
37
|
+
"types": "./src/status-page/pages/*.astro",
|
|
38
|
+
"import": "./src/status-page/pages/*.astro",
|
|
39
|
+
"default": "./src/status-page/pages/*.astro"
|
|
40
|
+
},
|
|
41
|
+
"./status-page/pages/api/status.json": {
|
|
42
|
+
"types": "./dist/status-page/pages/api/status.json.d.ts",
|
|
43
|
+
"import": "./dist/status-page/pages/api/status.json.js",
|
|
44
|
+
"default": "./dist/status-page/pages/api/status.json.js"
|
|
45
|
+
},
|
|
46
|
+
"./schemas": {
|
|
47
|
+
"types": "./dist/schemas/index.d.ts",
|
|
48
|
+
"import": "./dist/schemas/index.js"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist",
|
|
53
|
+
"src/status-page",
|
|
54
|
+
"src/schemas/migrations",
|
|
55
|
+
"README.md",
|
|
56
|
+
"CHANGELOG.md",
|
|
57
|
+
"SPEC.md"
|
|
58
|
+
],
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public",
|
|
61
|
+
"registry": "https://registry.npmjs.org/"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"astro": "^6.1.10"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@growth-labs/analytics": "^0.3.0",
|
|
68
|
+
"@growth-labs/notify": "^0.1.0",
|
|
69
|
+
"drizzle-orm": "^0.45.2",
|
|
70
|
+
"zod": "^3.24.0"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@cloudflare/workers-types": "^4.20260402.1",
|
|
74
|
+
"astro": "^6.1.10",
|
|
75
|
+
"typescript": "^5.7.0",
|
|
76
|
+
"vitest": "^3.0.0"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "tsc && node -e \"require('node:fs').cpSync('src/schemas/migrations','dist/schemas/migrations',{recursive:true})\"",
|
|
80
|
+
"dev": "tsc --watch",
|
|
81
|
+
"test": "vitest run",
|
|
82
|
+
"test:watch": "vitest",
|
|
83
|
+
"check": "biome check src/ __tests__ package.json README.md CHANGELOG.md SPEC.md"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
CREATE TABLE gl_uptime_checks (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
surface TEXT NOT NULL,
|
|
4
|
+
check_type TEXT NOT NULL,
|
|
5
|
+
status TEXT NOT NULL,
|
|
6
|
+
status_code INTEGER,
|
|
7
|
+
latency_ms INTEGER,
|
|
8
|
+
error_message TEXT,
|
|
9
|
+
checked_at INTEGER NOT NULL
|
|
10
|
+
);
|
|
11
|
+
CREATE INDEX idx_uptime_surface_time ON gl_uptime_checks (surface, checked_at DESC);
|
|
12
|
+
CREATE INDEX idx_uptime_status_time ON gl_uptime_checks (status, checked_at DESC);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
CREATE TABLE gl_uptime_incidents (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
surface TEXT NOT NULL,
|
|
4
|
+
opened_at INTEGER NOT NULL,
|
|
5
|
+
closed_at INTEGER,
|
|
6
|
+
trigger_check_id TEXT,
|
|
7
|
+
resolve_check_id TEXT,
|
|
8
|
+
severity TEXT NOT NULL,
|
|
9
|
+
notes TEXT
|
|
10
|
+
);
|
|
11
|
+
CREATE INDEX idx_incidents_surface_open ON gl_uptime_incidents (surface, opened_at DESC);
|
|
12
|
+
CREATE INDEX idx_incidents_open ON gl_uptime_incidents (closed_at) WHERE closed_at IS NULL;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
CREATE TABLE gl_errors (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
realm_key TEXT NOT NULL,
|
|
4
|
+
surface TEXT NOT NULL,
|
|
5
|
+
severity TEXT NOT NULL,
|
|
6
|
+
message TEXT NOT NULL,
|
|
7
|
+
stack TEXT,
|
|
8
|
+
request_id TEXT,
|
|
9
|
+
status_code INTEGER,
|
|
10
|
+
duration_ms INTEGER,
|
|
11
|
+
occurred_at INTEGER NOT NULL,
|
|
12
|
+
fingerprint TEXT NOT NULL
|
|
13
|
+
);
|
|
14
|
+
CREATE INDEX idx_errors_realm_time ON gl_errors (realm_key, occurred_at DESC);
|
|
15
|
+
CREATE INDEX idx_errors_fingerprint_time ON gl_errors (fingerprint, occurred_at DESC);
|
|
16
|
+
CREATE INDEX idx_errors_surface_time ON gl_errors (surface, occurred_at DESC);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @growth-labs/monitoring D1 Migrations
|
|
2
|
+
|
|
3
|
+
Apply these migrations to the consumer's monitoring D1 database before mounting
|
|
4
|
+
the prober, tail worker, or status page.
|
|
5
|
+
|
|
6
|
+
Wrangler example:
|
|
7
|
+
|
|
8
|
+
```toml
|
|
9
|
+
[[d1_databases]]
|
|
10
|
+
binding = "MONITORING_DB"
|
|
11
|
+
database_name = "monitoring"
|
|
12
|
+
migrations_dir = "node_modules/@growth-labs/monitoring/src/schemas/migrations"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Tables are prefixed with `gl_` per Growth Labs package convention.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Monitoring Status Page
|
|
2
|
+
|
|
3
|
+
`createStatusPageApp(config)` is an Astro integration for a read-only status
|
|
4
|
+
Worker.
|
|
5
|
+
|
|
6
|
+
Defaults:
|
|
7
|
+
|
|
8
|
+
- HTML status route: `/`
|
|
9
|
+
- JSON snapshot route: `/api/status.json`
|
|
10
|
+
- D1 binding: `MONITORING_DB`
|
|
11
|
+
|
|
12
|
+
The page reads current surface status, open incidents, and top 24-hour error
|
|
13
|
+
fingerprints from the monitoring D1 tables. It performs no mutations and has no
|
|
14
|
+
auth dependency.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro'
|
|
2
|
+
import type { StatusPageConfig } from '../types.js'
|
|
3
|
+
|
|
4
|
+
export interface StatusPageAppConfig extends StatusPageConfig {
|
|
5
|
+
routePath?: string
|
|
6
|
+
jsonRoutePath?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createStatusPageApp(config: StatusPageAppConfig): AstroIntegration {
|
|
10
|
+
const routePath = config.routePath ?? '/'
|
|
11
|
+
const jsonRoutePath = config.jsonRoutePath ?? '/api/status.json'
|
|
12
|
+
const serializable = {
|
|
13
|
+
realm: config.realm,
|
|
14
|
+
d1Binding: config.d1Binding ?? 'MONITORING_DB',
|
|
15
|
+
surfaces: config.surfaces,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
name: '@growth-labs/monitoring/status-page',
|
|
20
|
+
hooks: {
|
|
21
|
+
'astro:config:setup': ({ injectRoute, updateConfig }) => {
|
|
22
|
+
updateConfig({
|
|
23
|
+
vite: {
|
|
24
|
+
plugins: [
|
|
25
|
+
{
|
|
26
|
+
name: '@growth-labs/monitoring:status-page-config',
|
|
27
|
+
resolveId(id: string) {
|
|
28
|
+
if (id === 'virtual:growth-labs/monitoring/status-page/config') return id
|
|
29
|
+
},
|
|
30
|
+
load(id: string) {
|
|
31
|
+
if (id === 'virtual:growth-labs/monitoring/status-page/config') {
|
|
32
|
+
return `export const config = ${JSON.stringify(serializable)}`
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
injectRoute({
|
|
40
|
+
pattern: routePath,
|
|
41
|
+
entrypoint: '@growth-labs/monitoring/status-page/pages/index',
|
|
42
|
+
})
|
|
43
|
+
injectRoute({
|
|
44
|
+
pattern: jsonRoutePath,
|
|
45
|
+
entrypoint: '@growth-labs/monitoring/status-page/pages/api/status.json',
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type { ErrorRollup, StatusPageConfig, SurfaceStatus, UptimeIncident } from '../types.js'
|
|
53
|
+
export {
|
|
54
|
+
getCurrentSurfaceStatuses,
|
|
55
|
+
getOpenIncidents,
|
|
56
|
+
getSurfaceUptime,
|
|
57
|
+
getTopErrors,
|
|
58
|
+
} from './lib/queries.js'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { ErrorRollup as ErrorRollupItem } from '../../types.js'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
errors: ErrorRollupItem[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { errors } = Astro.props
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<section class="grid content-start gap-3">
|
|
12
|
+
<h2 class="font-semibold text-lg">Top errors</h2>
|
|
13
|
+
{errors.length === 0 ? (
|
|
14
|
+
<p class="text-sm text-zinc-400">No recent errors</p>
|
|
15
|
+
) : (
|
|
16
|
+
<ul class="grid gap-2">
|
|
17
|
+
{errors.map((error) => (
|
|
18
|
+
<li class="border-zinc-800 border-b py-2">
|
|
19
|
+
<p class="font-medium text-sm">{error.surface}</p>
|
|
20
|
+
<p class="truncate text-sm text-zinc-400">{error.message}</p>
|
|
21
|
+
<p class="text-xs text-zinc-500">{error.count} · {error.fingerprint}</p>
|
|
22
|
+
</li>
|
|
23
|
+
))}
|
|
24
|
+
</ul>
|
|
25
|
+
)}
|
|
26
|
+
</section>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { UptimeIncident } from '../../types.js'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
incidents: UptimeIncident[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { incidents } = Astro.props
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<section class="grid content-start gap-3">
|
|
12
|
+
<h2 class="font-semibold text-lg">Recent incidents</h2>
|
|
13
|
+
{incidents.length === 0 ? (
|
|
14
|
+
<p class="text-sm text-zinc-400">No open incidents</p>
|
|
15
|
+
) : (
|
|
16
|
+
<ul class="grid gap-2">
|
|
17
|
+
{incidents.map((incident) => (
|
|
18
|
+
<li class="border-zinc-800 border-b py-2">
|
|
19
|
+
<p class="font-medium text-sm">{incident.surface}</p>
|
|
20
|
+
<p class="text-sm text-zinc-400">{incident.severity} · opened {new Date(incident.opened_at * 1000).toUTCString()}</p>
|
|
21
|
+
</li>
|
|
22
|
+
))}
|
|
23
|
+
</ul>
|
|
24
|
+
)}
|
|
25
|
+
</section>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { SurfaceStatus } from '../../types.js'
|
|
3
|
+
import { formatUptime, relativeTime } from '../shell.js'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
surface: SurfaceStatus
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { surface } = Astro.props
|
|
10
|
+
const pillClass = {
|
|
11
|
+
green: 'bg-emerald-400/15 text-emerald-200 ring-emerald-400/30',
|
|
12
|
+
yellow: 'bg-amber-400/15 text-amber-200 ring-amber-400/30',
|
|
13
|
+
red: 'bg-red-400/15 text-red-200 ring-red-400/30',
|
|
14
|
+
}[surface.status]
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<article class="grid grid-cols-[auto_1fr_auto] items-center gap-4 border-zinc-800 border-b py-3" data-surface={surface.name}>
|
|
18
|
+
<span class:list={['rounded px-2 py-1 text-xs ring-1', pillClass]}>{surface.status}</span>
|
|
19
|
+
<h2 class="truncate font-medium text-base">{surface.name}</h2>
|
|
20
|
+
<div class="text-right text-sm text-zinc-400">
|
|
21
|
+
<p>{relativeTime(surface.lastCheckedAt)}</p>
|
|
22
|
+
<p>{formatUptime(surface.uptime7d)} 7d</p>
|
|
23
|
+
</div>
|
|
24
|
+
</article>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ErrorRollup,
|
|
3
|
+
StatusPageSurfaceConfig,
|
|
4
|
+
SurfaceStatus,
|
|
5
|
+
UptimeIncident,
|
|
6
|
+
} from '../../types.js'
|
|
7
|
+
|
|
8
|
+
interface CheckRow {
|
|
9
|
+
id: string
|
|
10
|
+
surface: string
|
|
11
|
+
status: string
|
|
12
|
+
checked_at: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ErrorRollupRow {
|
|
16
|
+
fingerprint: string
|
|
17
|
+
surface: string
|
|
18
|
+
message: string
|
|
19
|
+
count: number
|
|
20
|
+
occurred_at: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getCurrentSurfaceStatuses(
|
|
24
|
+
db: D1Database,
|
|
25
|
+
surfaces: StatusPageSurfaceConfig[],
|
|
26
|
+
): Promise<SurfaceStatus[]> {
|
|
27
|
+
const openIncidents = await getOpenIncidents(db)
|
|
28
|
+
return Promise.all(
|
|
29
|
+
surfaces.map(async (surface) => {
|
|
30
|
+
const lastCheck = await db
|
|
31
|
+
.prepare(`
|
|
32
|
+
SELECT id, surface, status, checked_at
|
|
33
|
+
FROM gl_uptime_checks
|
|
34
|
+
WHERE surface = ?
|
|
35
|
+
ORDER BY checked_at DESC
|
|
36
|
+
LIMIT 1
|
|
37
|
+
`)
|
|
38
|
+
.bind(surface.name)
|
|
39
|
+
.first<CheckRow>()
|
|
40
|
+
const incident = openIncidents.find((row) => row.surface === surface.name)
|
|
41
|
+
const uptime7d = await getSurfaceUptime(db, surface.name, 7)
|
|
42
|
+
return {
|
|
43
|
+
name: surface.name,
|
|
44
|
+
status: statusFor(lastCheck ?? null, incident),
|
|
45
|
+
lastCheckedAt: lastCheck?.checked_at ?? null,
|
|
46
|
+
uptime7d,
|
|
47
|
+
}
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getOpenIncidents(db: D1Database): Promise<UptimeIncident[]> {
|
|
53
|
+
const { results } = await db
|
|
54
|
+
.prepare(`
|
|
55
|
+
SELECT id, surface, opened_at, closed_at, trigger_check_id, resolve_check_id, severity, notes
|
|
56
|
+
FROM gl_uptime_incidents
|
|
57
|
+
WHERE closed_at IS NULL
|
|
58
|
+
ORDER BY opened_at DESC
|
|
59
|
+
`)
|
|
60
|
+
.all<UptimeIncident>()
|
|
61
|
+
return results ?? []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function getTopErrors(db: D1Database, hoursBack: number): Promise<ErrorRollup[]> {
|
|
65
|
+
const since = Math.floor(Date.now() / 1000) - hoursBack * 60 * 60
|
|
66
|
+
const { results } = await db
|
|
67
|
+
.prepare(`
|
|
68
|
+
SELECT fingerprint, surface, message, COUNT(*) AS count, MAX(occurred_at) AS occurred_at
|
|
69
|
+
FROM gl_errors
|
|
70
|
+
WHERE occurred_at >= ?
|
|
71
|
+
GROUP BY fingerprint
|
|
72
|
+
ORDER BY count DESC, occurred_at DESC
|
|
73
|
+
LIMIT 5
|
|
74
|
+
`)
|
|
75
|
+
.bind(since)
|
|
76
|
+
.all<ErrorRollupRow>()
|
|
77
|
+
return (results ?? []).map((row) => ({
|
|
78
|
+
fingerprint: row.fingerprint,
|
|
79
|
+
surface: row.surface,
|
|
80
|
+
message: row.message,
|
|
81
|
+
count: Number(row.count),
|
|
82
|
+
lastOccurredAt: Number(row.occurred_at),
|
|
83
|
+
}))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function getSurfaceUptime(
|
|
87
|
+
db: D1Database,
|
|
88
|
+
surface: string,
|
|
89
|
+
daysBack: number,
|
|
90
|
+
): Promise<number> {
|
|
91
|
+
const since = Math.floor(Date.now() / 1000) - daysBack * 24 * 60 * 60
|
|
92
|
+
const { results } = await db
|
|
93
|
+
.prepare(`
|
|
94
|
+
SELECT id, surface, status, checked_at
|
|
95
|
+
FROM gl_uptime_checks
|
|
96
|
+
WHERE surface = ? AND checked_at >= ?
|
|
97
|
+
ORDER BY checked_at DESC
|
|
98
|
+
`)
|
|
99
|
+
.bind(surface, since)
|
|
100
|
+
.all<CheckRow>()
|
|
101
|
+
const rows = results ?? []
|
|
102
|
+
if (rows.length === 0) return 1
|
|
103
|
+
return rows.filter((row) => row.status === 'pass').length / rows.length
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function statusFor(
|
|
107
|
+
check: CheckRow | null,
|
|
108
|
+
incident: UptimeIncident | undefined,
|
|
109
|
+
): SurfaceStatus['status'] {
|
|
110
|
+
if (incident?.severity === 'critical') return 'red'
|
|
111
|
+
if (incident?.severity === 'warning') return 'yellow'
|
|
112
|
+
if (!check) return 'yellow'
|
|
113
|
+
return check.status === 'pass' ? 'green' : 'yellow'
|
|
114
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { config } from 'virtual:growth-labs/monitoring/status-page/config'
|
|
2
|
+
import type { APIRoute } from 'astro'
|
|
3
|
+
import { getCurrentSurfaceStatuses, getOpenIncidents, getTopErrors } from '../../lib/queries.js'
|
|
4
|
+
|
|
5
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
6
|
+
const env = (locals as { runtime?: { env?: Record<string, unknown> } }).runtime?.env ?? {}
|
|
7
|
+
const db = env[config.d1Binding] as D1Database
|
|
8
|
+
const [surfaces, openIncidents, topErrors] = await Promise.all([
|
|
9
|
+
getCurrentSurfaceStatuses(db, config.surfaces),
|
|
10
|
+
getOpenIncidents(db),
|
|
11
|
+
getTopErrors(db, 24),
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
return new Response(
|
|
15
|
+
JSON.stringify({
|
|
16
|
+
realm: config.realm,
|
|
17
|
+
generatedAt: Math.floor(Date.now() / 1000),
|
|
18
|
+
surfaces,
|
|
19
|
+
openIncidents,
|
|
20
|
+
topErrors,
|
|
21
|
+
}),
|
|
22
|
+
{ headers: { 'Content-Type': 'application/json' } },
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { config } from 'virtual:growth-labs/monitoring/status-page/config'
|
|
3
|
+
import IncidentList from '../components/IncidentList.astro'
|
|
4
|
+
import SurfaceRow from '../components/SurfaceRow.astro'
|
|
5
|
+
import ErrorRollup from '../components/ErrorRollup.astro'
|
|
6
|
+
import { getCurrentSurfaceStatuses, getOpenIncidents, getTopErrors } from '../lib/queries.js'
|
|
7
|
+
|
|
8
|
+
const env = Astro.locals.runtime?.env ?? {}
|
|
9
|
+
const db = env[config.d1Binding] as D1Database
|
|
10
|
+
const [surfaces, incidents, topErrors] = await Promise.all([
|
|
11
|
+
getCurrentSurfaceStatuses(db, config.surfaces),
|
|
12
|
+
getOpenIncidents(db),
|
|
13
|
+
getTopErrors(db, 24),
|
|
14
|
+
])
|
|
15
|
+
const generatedAt = Math.floor(Date.now() / 1000)
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<!doctype html>
|
|
19
|
+
<html lang="en">
|
|
20
|
+
<head>
|
|
21
|
+
<meta charset="utf-8" />
|
|
22
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
23
|
+
<title>{config.realm} status</title>
|
|
24
|
+
</head>
|
|
25
|
+
<body class="min-h-screen bg-zinc-950 text-zinc-100">
|
|
26
|
+
<main class="mx-auto flex max-w-4xl flex-col gap-8 px-4 py-8">
|
|
27
|
+
<header class="flex flex-wrap items-end justify-between gap-4 border-zinc-800 border-b pb-4">
|
|
28
|
+
<div>
|
|
29
|
+
<p class="text-sm text-zinc-400">Status</p>
|
|
30
|
+
<h1 class="font-semibold text-3xl tracking-normal">{config.realm}</h1>
|
|
31
|
+
</div>
|
|
32
|
+
<p class="text-sm text-zinc-400">Updated <time datetime={new Date(generatedAt * 1000).toISOString()}>{new Date(generatedAt * 1000).toUTCString()}</time></p>
|
|
33
|
+
</header>
|
|
34
|
+
|
|
35
|
+
<section class="grid gap-2" data-monitoring-status-surfaces>
|
|
36
|
+
{surfaces.map((surface) => <SurfaceRow surface={surface} />)}
|
|
37
|
+
</section>
|
|
38
|
+
|
|
39
|
+
<section class="grid gap-4 md:grid-cols-2">
|
|
40
|
+
<IncidentList incidents={incidents.slice(0, 5)} />
|
|
41
|
+
<ErrorRollup errors={topErrors.slice(0, 5)} />
|
|
42
|
+
</section>
|
|
43
|
+
</main>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function relativeTime(
|
|
2
|
+
timestampSeconds: number | null,
|
|
3
|
+
nowSeconds = Math.floor(Date.now() / 1000),
|
|
4
|
+
): string {
|
|
5
|
+
if (!timestampSeconds) return 'never'
|
|
6
|
+
const diff = Math.max(0, nowSeconds - timestampSeconds)
|
|
7
|
+
if (diff < 60) return `${diff}s ago`
|
|
8
|
+
const minutes = Math.floor(diff / 60)
|
|
9
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
10
|
+
const hours = Math.floor(minutes / 60)
|
|
11
|
+
if (hours < 24) return `${hours}h ago`
|
|
12
|
+
return `${Math.floor(hours / 24)}d ago`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatUptime(value: number): string {
|
|
16
|
+
return `${(value * 100).toFixed(2)}%`
|
|
17
|
+
}
|