Deployment monitoring and recovery tooling
This commit is contained in:
Executable
+63
@@ -0,0 +1,63 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { isIP } from 'node:net';
|
||||
import { monitorEventLoopDelay } from 'node:perf_hooks';
|
||||
import { stat,statfs } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import type { AddressInfo } from 'node:net';
|
||||
|
||||
/** No hop-count/boolean trust. Match only explicit proxy addresses/subnets. The edge must
|
||||
* overwrite forwarded headers and prevent direct backend access from trusted addresses. */
|
||||
export function trustedProxies(env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
const raw = env.WIKIDECK_TRUSTED_PROXIES;
|
||||
if (!raw) return [];
|
||||
const values=raw.split(',').map(s=>s.trim());
|
||||
if(values.length>16)throw new Error('At most 16 trusted proxy addresses/subnets');
|
||||
for(const value of values){
|
||||
const [ip,prefix,...extra]=value.split('/'),family=isIP(ip);
|
||||
if(!family||extra.length||(prefix!==undefined&&(!/^\d{1,3}$/.test(prefix)||Number(prefix)<(family===4?8:32)||Number(prefix)>(family===4?32:128))))
|
||||
throw new Error('Trusted proxies must be literal IPs or bounded CIDRs, never booleans, names or hop counts');
|
||||
}
|
||||
return [...new Set(values)];
|
||||
}
|
||||
export function operationsPort(env: NodeJS.ProcessEnv = process.env): number | undefined {
|
||||
if(env.WIKIDECK_OPS_PORT===undefined||env.WIKIDECK_OPS_PORT==='')return undefined;
|
||||
const n=Number(env.WIKIDECK_OPS_PORT);
|
||||
if(!/^\d+$/.test(env.WIKIDECK_OPS_PORT)||!Number.isInteger(n)||n<1||n>65535)throw new Error('Invalid WIKIDECK_OPS_PORT');
|
||||
return n;
|
||||
}
|
||||
/** Separate loopback listener, disabled by default. No bearer tokens or details on public RPC.
|
||||
* Local users/processes share this trust boundary; never forward/publish this port. */
|
||||
export function startOperations(port: number | undefined, snapshot: ()=>unknown, directory?:string) {
|
||||
if(port===undefined)return {close() {}};
|
||||
const delay=monitorEventLoopDelay({resolution:20});delay.enable();
|
||||
let disk:unknown=null,lastDiskSample=0;
|
||||
const sampleDisk=async()=>{
|
||||
if(!directory)return;
|
||||
try {
|
||||
const fs=await statfs(directory);
|
||||
const sizes=await Promise.all(['ledger.sqlite','ledger.sqlite-wal','market.sqlite','market.sqlite-wal'].map(async name=>{
|
||||
try{return {name,bytes:(await stat(join(directory,name))).size};}
|
||||
catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT')return {name,bytes:0};throw error;}
|
||||
}));
|
||||
disk={availableBytes:fs.bavail*fs.bsize,files:sizes};lastDiskSample=Date.now();
|
||||
}catch{disk={available:false,error:'Disk sample failed; inspect local filesystem and permissions.'};lastDiskSample=Date.now();}
|
||||
};
|
||||
void sampleDisk();const diskTimer=setInterval(()=>{void sampleDisk();},30000);diskTimer.unref();
|
||||
const server=createServer((req,res)=>{
|
||||
res.setHeader('Cache-Control','no-store');res.setHeader('Content-Type','application/json');
|
||||
if(req.method!=='GET'||req.url!=='/status'){res.writeHead(404).end('{"error":"Not found"}');return;}
|
||||
try {
|
||||
const data=JSON.stringify({schema:'wikideck-operations-v1',observedAt:new Date().toISOString(),
|
||||
uptimeSeconds:process.uptime(),nodeRssBytes:process.memoryUsage().rss,
|
||||
eventLoop:{meanMs:Number.isFinite(delay.mean)?delay.mean/1e6:0,maxMs:delay.max/1e6},
|
||||
disk,diskSampledAt:lastDiskSample||null,...snapshot() as object});
|
||||
if(Buffer.byteLength(data)>64*1024)throw new Error('Diagnostics size');
|
||||
res.end(data);
|
||||
}catch{res.writeHead(503).end('{"error":"Diagnostics unavailable"}');}
|
||||
});
|
||||
server.maxConnections=4;server.headersTimeout=5000;server.requestTimeout=5000;server.keepAliveTimeout=1000;
|
||||
server.setTimeout(5000,socket=>socket.destroy());
|
||||
server.on('error',()=>{clearInterval(diskTimer);delay.disable();console.error('Private operations listener failed; check local monitoring configuration.');});
|
||||
server.listen(port,'127.0.0.1');
|
||||
return {close(){clearInterval(diskTimer);delay.disable();server.closeAllConnections();server.close();},address:()=>server.address() as AddressInfo|null};
|
||||
}
|
||||
Reference in New Issue
Block a user