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

@@ -8,6 +8,11 @@
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
vi.mock('../../logger.js', () => ({
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), fatal: vi.fn() },
}));
import { logger } from '../../logger.js';
// ── mock the drizzle query chain: db.select(...).from(...).where(...) ────────
let users: { userId: string }[] = [];
const whereMock = vi.fn(async () => users);
@@ -35,6 +40,7 @@ beforeEach(() => {
whereMock.mockClear();
fromMock.mockClear();
selectMock.mockClear();
vi.clearAllMocks();
vi.useFakeTimers();
});
@@ -102,8 +108,6 @@ describe('startTodoistSyncScheduler', () => {
if (id === 'bad') throw new Error('todoist 401');
return [];
});
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
startTodoistSyncScheduler(60_000);
await vi.advanceTimersByTimeAsync(10_001);
@@ -112,19 +116,27 @@ describe('startTodoistSyncScheduler', () => {
await Promise.resolve();
expect(fetchSignalsMock).toHaveBeenCalledTimes(3);
expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('sync error'), expect.anything());
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('2 ok, 1 failed'));
expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({ err: expect.anything() }),
'scheduler: sync error',
);
expect(logger.info).toHaveBeenCalledWith(
expect.objectContaining({ ok: 2, failed: 1 }),
'scheduler: todoist sync',
);
});
it('survives a db query failure — logs and skips the tick', async () => {
const { startTodoistSyncScheduler } = await import('../scheduler.js');
whereMock.mockRejectedValueOnce(new Error('sqlite locked'));
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
startTodoistSyncScheduler(60_000);
await vi.advanceTimersByTimeAsync(10_001);
expect(fetchSignalsMock).not.toHaveBeenCalled();
expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('failed to query users'));
expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({ err: expect.anything() }),
'scheduler: failed to query users',
);
});
});