[claude] API observability — structured logging + /api/metrics endpoint (#57) (#87)

This commit was merged in pull request #87.
This commit is contained in:
2026-03-23 20:10:40 +00:00
parent 113095d2f0
commit 5dc71e1257
6 changed files with 75 additions and 0 deletions

View File

@@ -4,7 +4,19 @@ export interface LogContext {
[key: string]: unknown;
}
const LEVEL_ORDER: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };
function resolveMinLevel(): LogLevel {
const env = (process.env["LOG_LEVEL"] ?? "").toLowerCase();
if (env === "debug" || env === "info" || env === "warn" || env === "error") return env;
return "debug";
}
const minLevel: number = LEVEL_ORDER[resolveMinLevel()];
function emit(level: LogLevel, component: string, message: string, ctx?: LogContext): void {
if (LEVEL_ORDER[level] < minLevel) return;
const line: Record<string, unknown> = {
timestamp: new Date().toISOString(),
level,

View File

@@ -1,6 +1,7 @@
import { db, jobs, invoices } from "@workspace/db";
import { sql } from "drizzle-orm";
import { latencyHistogram, type BucketStats } from "./histogram.js";
import { requestCounters, type RequestCountsSnapshot } from "./request-counters.js";
export interface JobStateCounts {
awaiting_eval: number;
@@ -12,6 +13,7 @@ export interface JobStateCounts {
export interface MetricsSnapshot {
uptime_s: number;
http: RequestCountsSnapshot;
jobs: {
total: number;
by_state: JobStateCounts;
@@ -94,6 +96,7 @@ export class MetricsService {
return {
uptime_s: Math.floor((Date.now() - START_TIME) / 1000),
http: requestCounters.snapshot(),
jobs: {
total: jobsTotal,
by_state: byState,

View File

@@ -0,0 +1,37 @@
/** In-memory HTTP request counters for the /api/metrics endpoint. */
export interface RequestCountsSnapshot {
total: number;
by_status: Record<string, number>;
errors_4xx: number;
errors_5xx: number;
}
class RequestCounters {
private total = 0;
private byStatus: Record<number, number> = {};
private errors4xx = 0;
private errors5xx = 0;
record(statusCode: number): void {
this.total++;
this.byStatus[statusCode] = (this.byStatus[statusCode] ?? 0) + 1;
if (statusCode >= 400 && statusCode < 500) this.errors4xx++;
else if (statusCode >= 500) this.errors5xx++;
}
snapshot(): RequestCountsSnapshot {
const byStatus: Record<string, number> = {};
for (const [code, count] of Object.entries(this.byStatus)) {
byStatus[code] = count;
}
return {
total: this.total,
by_status: byStatus,
errors_4xx: this.errors4xx,
errors_5xx: this.errors5xx,
};
}
}
export const requestCounters = new RequestCounters();