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:
2026-04-26 03:37:28 +00:00
parent 7281af83a4
commit c4960d0601
18 changed files with 1041 additions and 64 deletions

View File

@@ -121,13 +121,14 @@ describe('connectNats — bridge bus → JetStream', () => {
it('swallows JetStream publish errors so the in-process bus keeps working', async () => {
const { connectNats } = await import('../nats.js');
const { logger } = await import('../../logger.js');
const { bus } = await import('../bus.js');
await connectNats('nats://test:4222');
// Force the next js.publish to reject.
lastJsPublish.mockRejectedValueOnce(new Error('jetstream down'));
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const errSpy = vi.spyOn(logger, 'error');
expect(() =>
bus.publish('signals.task.synced', { userId: 'u', source: 'todoist', count: 0, syncedAt: '' }),
@@ -142,12 +143,16 @@ describe('connectNats — bridge bus → JetStream', () => {
describe('connectNats — failure mode', () => {
it('logs a warning and stays silent when connect rejects', async () => {
const { connectNats } = await import('../nats.js');
const { logger } = await import('../../logger.js');
lastConnect.mockRejectedValueOnce(new Error('ECONNREFUSED'));
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const warnSpy = vi.spyOn(logger, 'warn');
await expect(connectNats('nats://nope:4222')).resolves.toBeUndefined();
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('connection failed'));
expect(warnSpy).toHaveBeenCalledWith(
expect.objectContaining({ err: expect.anything() }),
expect.stringContaining('connection failed'),
);
});
});

View File

@@ -12,6 +12,7 @@
import type { NatsConnection, JetStreamClient, StreamConfig } from 'nats';
import { bus } from './bus.js';
import { logger } from '../logger.js';
let nc: NatsConnection | null = null;
let js: JetStreamClient | null = null;
@@ -67,13 +68,13 @@ export async function connectNats(natsUrl: string): Promise<void> {
if (!js) return;
const data = new TextEncoder().encode(JSON.stringify(payload));
js.publish(subject, data).catch((err: Error) =>
console.error(`[nats] publish failed for ${subject}: ${err.message}`),
logger.error({ err, subject }, 'nats publish failed'),
);
});
console.log(`[nats] connected to ${natsUrl}, streams: ${STREAMS.map((s) => s.name).join(', ')}`);
logger.info({ url: natsUrl, streams: STREAMS.map((s) => s.name) }, 'nats connected');
} catch (err: any) {
console.warn(`[nats] connection failed — running without JetStream: ${err.message}`);
logger.warn({ err }, 'nats connection failed — running without JetStream');
}
}