From c7358a3ef50c9f88eefc20bb110ffcbe234f3079 Mon Sep 17 00:00:00 2001 From: autocommit Date: Sun, 12 Apr 2026 19:01:17 -0700 Subject: [PATCH] =?UTF-8?q?infra(relationship-worker):=20=F0=9F=A7=B1=20St?= =?UTF-8?q?andardize=20health=20check=20endpoints=20and=20logging=20config?= =?UTF-8?q?uration=20for=20improved=20observability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- services/relationship-worker/src/config.ts | 40 +++++++++++++++++ .../relationship-worker/src/health-server.ts | 43 +++++++++++++++++++ services/relationship-worker/src/logger.ts | 26 +++++++++++ 3 files changed, 109 insertions(+) create mode 100644 services/relationship-worker/src/config.ts create mode 100644 services/relationship-worker/src/health-server.ts create mode 100644 services/relationship-worker/src/logger.ts diff --git a/services/relationship-worker/src/config.ts b/services/relationship-worker/src/config.ts new file mode 100644 index 0000000..87e3c05 --- /dev/null +++ b/services/relationship-worker/src/config.ts @@ -0,0 +1,40 @@ +export interface WorkerConfig { + databaseUrl: string; + modelBossUrl: string; + modelBossApiKey: string | null; + modelBossModel: string; + healthPort: number; +} + +function required(name: string): string { + const value = process.env[name]; + if (!value || value.trim().length === 0) { + throw new Error(`Missing required env var: ${name}`); + } + return value.trim(); +} + +function optional(name: string, fallback: string): string { + const value = process.env[name]; + return value && value.trim().length > 0 ? value.trim() : fallback; +} + +function intEnv(name: string, fallback: number): number { + const raw = process.env[name]; + if (!raw) return fallback; + const parsed = parseInt(raw, 10); + if (isNaN(parsed)) { + throw new Error(`Env var ${name} must be a number, got "${raw}"`); + } + return parsed; +} + +export function loadConfig(): WorkerConfig { + return { + databaseUrl: required('DATABASE_URL'), + modelBossUrl: required('MODEL_BOSS_URL'), + modelBossApiKey: process.env.MODEL_BOSS_API_KEY?.trim() || null, + modelBossModel: optional('MODEL_BOSS_MODEL', 'auto'), + healthPort: intEnv('HEALTH_PORT', 3803), + }; +} diff --git a/services/relationship-worker/src/health-server.ts b/services/relationship-worker/src/health-server.ts new file mode 100644 index 0000000..a1e893e --- /dev/null +++ b/services/relationship-worker/src/health-server.ts @@ -0,0 +1,43 @@ +import { createServer, type Server } from 'node:http'; + +import { log } from './logger'; + +export interface HealthState { + modelBossReachable: boolean; + lastJobAt: Date | null; + lastCompletedAt: Date | null; +} + +export function startHealthServer(port: number, getState: () => HealthState): Server { + const server = createServer((req, res) => { + if (req.url === '/health/live') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ status: 'live' })); + return; + } + + if (req.url === '/health' || req.url === '/health/ready') { + const state = getState(); + const status = state.modelBossReachable ? 200 : 503; + res.writeHead(status, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + status: state.modelBossReachable ? 'ok' : 'degraded', + modelBossReachable: state.modelBossReachable, + lastJobAt: state.lastJobAt?.toISOString() ?? null, + lastCompletedAt: state.lastCompletedAt?.toISOString() ?? null, + }), + ); + return; + } + + res.writeHead(404); + res.end(); + }); + + server.listen(port, () => { + log.info('Health server listening', { port }); + }); + + return server; +} diff --git a/services/relationship-worker/src/logger.ts b/services/relationship-worker/src/logger.ts new file mode 100644 index 0000000..7024b89 --- /dev/null +++ b/services/relationship-worker/src/logger.ts @@ -0,0 +1,26 @@ +type Level = 'debug' | 'info' | 'warn' | 'error'; + +function emit(level: Level, message: string, data?: object): void { + const entry = { + timestamp: new Date().toISOString(), + level, + service: 'relationship-worker', + message, + ...(data ?? {}), + }; + const line = JSON.stringify(entry); + if (level === 'error') { + console.error(line); + } else if (level === 'warn') { + console.warn(line); + } else { + console.log(line); + } +} + +export const log = { + debug: (message: string, data?: object) => emit('debug', message, data), + info: (message: string, data?: object) => emit('info', message, data), + warn: (message: string, data?: object) => emit('warn', message, data), + error: (message: string, data?: object) => emit('error', message, data), +};