public uptime monitoring + (soon) observability with events saved to PDS
1import { Client, CredentialManager, ok } from '@atcute/client';
2import type {} from '@atcute/atproto';
3import type { Did } from '@atcute/lexicons';
4import { readFile } from 'node:fs/promises';
5import { checkService } from './pinger.ts';
6import type { ServiceConfig, UptimeCheck } from './types.ts';
7
8/**
9 * main worker function that monitors services and publishes uptime checks to AT Protocol
10 */
11async function main() {
12 // load configuration
13 const configPath = new URL('../config.json', import.meta.url);
14 const configData = await readFile(configPath, 'utf-8');
15 const config = JSON.parse(configData) as {
16 pds: string;
17 identifier: string;
18 password: string;
19 services: ServiceConfig[];
20 checkInterval: number;
21 };
22
23 // initialize AT Protocol client with authentication
24 const manager = new CredentialManager({ service: config.pds });
25 const rpc = new Client({ handler: manager });
26
27 // authenticate with app password
28 await manager.login({
29 identifier: config.identifier,
30 password: config.password,
31 });
32
33 console.log(`authenticated as ${manager.session?.did}`);
34
35 // function to perform checks and publish results
36 const performChecks = async () => {
37 console.log(`\nchecking ${config.services.length} services...`);
38
39 // run all checks concurrently
40 const checkPromises = config.services.map(async (service) => {
41 try {
42 const check = await checkService(service);
43 await publishCheck(rpc, manager.session!.did, check);
44 console.log(
45 `✓ ${service.name}: ${check.status} (${check.responseTime}ms)`,
46 );
47 } catch (error) {
48 console.error(`✗ ${service.name}: error publishing check`, error);
49 }
50 });
51
52 // wait for all checks to complete
53 await Promise.all(checkPromises);
54 };
55
56 // run checks immediately
57 await performChecks();
58
59 // schedule periodic checks
60 console.log(`scheduling checks every ${config.checkInterval} seconds`);
61 const interval = setInterval(performChecks, config.checkInterval * 1000);
62
63 // keep the process alive
64 interval.unref();
65 process.stdin.resume();
66
67 // handle graceful shutdown
68 process.on('SIGINT', () => {
69 console.log('\nshutting down gracefully...');
70 clearInterval(interval);
71 process.exit(0);
72 });
73
74 process.on('SIGTERM', () => {
75 console.log('\nshutting down gracefully...');
76 clearInterval(interval);
77 process.exit(0);
78 });
79}
80
81/**
82 * publishes an uptime check record to the PDS
83 *
84 * @param rpc the client instance
85 * @param did the DID of the authenticated user
86 * @param check the uptime check data to publish
87 */
88async function publishCheck(rpc: Client, did: Did, check: UptimeCheck) {
89 await ok(
90 rpc.post('com.atproto.repo.createRecord', {
91 input: {
92 repo: did,
93 collection: 'pet.nkp.uptime.check',
94 record: {
95 ...(check.groupName && { groupName: check.groupName }),
96 serviceName: check.serviceName,
97 ...(check.region && { region: check.region }),
98 serviceUrl: check.serviceUrl,
99 checkedAt: check.checkedAt,
100 status: check.status,
101 responseTime: check.responseTime,
102 ...(check.httpStatus && { httpStatus: check.httpStatus }),
103 ...(check.errorMessage && { errorMessage: check.errorMessage }),
104 },
105 },
106 }),
107 );
108}
109
110main().catch((error) => {
111 console.error('fatal error:', error);
112 process.exit(1);
113});