feat(observability): structured logs, W3C trace IDs, Sentry hooks (#18)
- TS: pino + pino-http; every HTTP request log includes traceId from W3C traceparent header (generated if absent); forwarded to ml/serving on all /score, /generate, /reward, and /api/ml proxy calls - Python: structlog JSON; FastAPI middleware binds trace_id via contextvars so every log line within a request carries it - Sentry: optional SENTRY_DSN init in both runtimes (no-op if unset) - Replace all console.* calls across services/api with pino logger - Update tests to spy on logger instead of console Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import 'dotenv/config';
|
||||
import { logger } from './logger.js';
|
||||
import express from 'express';
|
||||
import { pinoHttp } from 'pino-http';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import cors from 'cors';
|
||||
import { tracingMiddleware } from './middleware/tracing.js';
|
||||
import { config } from './config.js';
|
||||
import { db, runMigrations } from './db/index.js';
|
||||
import { tipScores, tipFeedback } from './db/schema.js';
|
||||
@@ -26,13 +29,11 @@ import { registerProfileSubscriptions } from './profile/subscriber.js';
|
||||
await mkdir(dirname(config.DATABASE_PATH), { recursive: true });
|
||||
runMigrations();
|
||||
|
||||
// Keep the API alive on stray async faults (e.g. a single bad admin route)
|
||||
// rather than dropping the whole process.
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('[api] unhandledRejection', reason);
|
||||
logger.error({ err: reason }, 'unhandledRejection');
|
||||
});
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[api] uncaughtException', err);
|
||||
logger.fatal({ err }, 'uncaughtException');
|
||||
});
|
||||
|
||||
const app = express();
|
||||
@@ -43,6 +44,15 @@ app.use(
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
app.use(tracingMiddleware);
|
||||
app.use(
|
||||
pinoHttp({
|
||||
logger,
|
||||
genReqId: (req) => req.traceId,
|
||||
customProps: (req) => ({ traceId: req.traceId }),
|
||||
autoLogging: { ignore: (req) => req.url === '/health' },
|
||||
}),
|
||||
);
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use(sessionMiddleware);
|
||||
@@ -56,16 +66,13 @@ app.use('/api/user', userRouter);
|
||||
app.use('/api/push', pushRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
|
||||
// Proxy ml/serving endpoints through the API (admin-only).
|
||||
// Allows admin UI to call /api/ml/stats/:userId, /api/ml/features/:userId
|
||||
// without needing direct access to the ml/serving port.
|
||||
app.use('/api/ml', requireAuth as any, requireAdmin as any, async (req: Request, res: Response) => {
|
||||
const mlUrl = config.ML_SERVING_URL;
|
||||
const target = `${mlUrl}${req.path}`;
|
||||
try {
|
||||
const upstream = await fetch(target, {
|
||||
method: req.method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: { 'Content-Type': 'application/json', traceparent: req.traceparent },
|
||||
body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined,
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
@@ -82,7 +89,7 @@ async function purgeExpiredData() {
|
||||
await db.delete(tipScores).where(lt(tipScores.servedAt, cutoff));
|
||||
await db.delete(tipFeedback).where(lt(tipFeedback.createdAt, cutoff));
|
||||
} catch (err: any) {
|
||||
console.error(`[purge] retention cleanup failed: ${err.message}`);
|
||||
logger.error({ err }, 'retention cleanup failed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +97,7 @@ purgeExpiredData();
|
||||
setInterval(purgeExpiredData, 24 * 60 * 60 * 1000);
|
||||
|
||||
app.listen(config.PORT, () => {
|
||||
console.log(`oO API listening on http://localhost:${config.PORT}`);
|
||||
logger.info({ port: config.PORT }, 'oO API listening');
|
||||
});
|
||||
|
||||
if (config.NATS_URL) {
|
||||
|
||||
Reference in New Issue
Block a user