fase(23): observability and health probes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
debian
2026-03-08 06:10:24 -04:00
parent ddb4f66036
commit 629eafecd8
5 changed files with 34 additions and 15 deletions

View File

@@ -385,14 +385,14 @@ Spec: `.ralph/specs/phase-18-cli-cicd.md`
--- ---
## Phase 23: Observability [PENDIENTE] ## Phase 23: Observability [COMPLETO]
- [ ] 23.1: Request correlation: requestId en CADA log entry via pino child logger - [x] 23.1: Request correlation: requestId en CADA log entry via pino child logger
- [ ] 23.2: Structured error logging con contexto (userId, sessionId, etc.) - [x] 23.2: Structured error logging con contexto (userId, sessionId, etc.)
- [ ] 23.3: Liveness probe: GET /health/live - [x] 23.3: Liveness probe: GET /health/live
- [ ] 23.4: Readiness probe: GET /health/ready (DB + job queue check) - [x] 23.4: Readiness probe: GET /health/ready (DB + job queue check)
- [ ] 23.5: Startup probe: medir tiempo de arranque, loguear - [x] 23.5: Startup probe: medir tiempo de arranque, loguear
- [ ] 23.6: Commit: `fase(23): observability and health probes` - [x] 23.6: Commit: `fase(23): observability and health probes`
--- ---

View File

@@ -51,10 +51,12 @@ class RateLimitError extends AppError {
} }
exports.RateLimitError = RateLimitError; exports.RateLimitError = RateLimitError;
function globalErrorHandler(err, req, res, _next) { function globalErrorHandler(err, req, res, _next) {
const logger = req.log; const authReq = req;
const logger = authReq.log;
const userId = authReq.user?.id;
if (err instanceof AppError && err.isOperational) { if (err instanceof AppError && err.isOperational) {
if (logger) { if (logger) {
logger.warn({ err, statusCode: err.statusCode }, err.message); logger.warn({ err, statusCode: err.statusCode, userId }, err.message);
} }
const body = { error: err.message, code: err.code }; const body = { error: err.message, code: err.code };
if (err instanceof ValidationError && err.details !== undefined) { if (err instanceof ValidationError && err.details !== undefined) {
@@ -64,7 +66,7 @@ function globalErrorHandler(err, req, res, _next) {
return; return;
} }
if (logger) { if (logger) {
logger.error({ err }, 'Unhandled error'); logger.error({ err, userId }, 'Unhandled error');
} }
else { else {
console.error('Unhandled error', err); console.error('Unhandled error', err);

5
dist/main.js vendored
View File

@@ -84,6 +84,8 @@ const ReportWorker_1 = require("./jobs/workers/ReportWorker");
const server_1 = require("./api/server"); const server_1 = require("./api/server");
const SocketGateway_1 = require("./realtime/SocketGateway"); const SocketGateway_1 = require("./realtime/SocketGateway");
async function bootstrap() { async function bootstrap() {
// Startup probe — measure total boot time
const startupAt = Date.now();
// 1. Config // 1. Config
const config = (0, Config_1.loadConfig)(); const config = (0, Config_1.loadConfig)();
// 2. Logger // 2. Logger
@@ -208,7 +210,8 @@ async function bootstrap() {
await new Promise((resolve) => { await new Promise((resolve) => {
httpServer.listen(config.port, config.host, resolve); httpServer.listen(config.port, config.host, resolve);
}); });
logger.info({ port: config.port, host: config.host }, 'ABE server ready'); const startupMs = Date.now() - startupAt;
logger.info({ port: config.port, host: config.host, startupMs }, 'ABE server ready');
// 14. Graceful shutdown // 14. Graceful shutdown
let shuttingDown = false; let shuttingDown = false;
async function shutdown(signal) { async function shutdown(signal) {

View File

@@ -49,17 +49,27 @@ export class RateLimitError extends AppError {
} }
} }
type ExtendedReq = Request & {
log?: {
warn(obj: Record<string, unknown>, msg: string): void;
error(obj: Record<string, unknown>, msg: string): void;
};
user?: { id: string; email: string; role: string };
};
export function globalErrorHandler( export function globalErrorHandler(
err: Error, err: Error,
req: Request, req: Request,
res: Response, res: Response,
_next: NextFunction, _next: NextFunction,
): void { ): void {
const logger = (req as Request & { log?: { warn: Function; error: Function } }).log; const authReq = req as ExtendedReq;
const logger = authReq.log;
const userId = authReq.user?.id;
if (err instanceof AppError && err.isOperational) { if (err instanceof AppError && err.isOperational) {
if (logger) { if (logger) {
logger.warn({ err, statusCode: err.statusCode }, err.message); logger.warn({ err, statusCode: err.statusCode, userId }, err.message);
} }
const body: Record<string, unknown> = { error: err.message, code: err.code }; const body: Record<string, unknown> = { error: err.message, code: err.code };
if (err instanceof ValidationError && err.details !== undefined) { if (err instanceof ValidationError && err.details !== undefined) {
@@ -70,7 +80,7 @@ export function globalErrorHandler(
} }
if (logger) { if (logger) {
logger.error({ err }, 'Unhandled error'); logger.error({ err, userId }, 'Unhandled error');
} else { } else {
console.error('Unhandled error', err); console.error('Unhandled error', err);
} }

View File

@@ -93,6 +93,9 @@ import { createServer } from './api/server';
import { SocketGateway } from './realtime/SocketGateway'; import { SocketGateway } from './realtime/SocketGateway';
async function bootstrap(): Promise<void> { async function bootstrap(): Promise<void> {
// Startup probe — measure total boot time
const startupAt = Date.now();
// 1. Config // 1. Config
const config = loadConfig(); const config = loadConfig();
@@ -247,7 +250,8 @@ async function bootstrap(): Promise<void> {
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
httpServer.listen(config.port, config.host, resolve); httpServer.listen(config.port, config.host, resolve);
}); });
logger.info({ port: config.port, host: config.host }, 'ABE server ready'); const startupMs = Date.now() - startupAt;
logger.info({ port: config.port, host: config.host, startupMs }, 'ABE server ready');
// 14. Graceful shutdown // 14. Graceful shutdown
let shuttingDown = false; let shuttingDown = false;