English · 简体中文
Observability — Deep Dive
Source:
server/src/lib/server-logs.ts,server/src/routes/logs.ts,server/src/lib/request-log.ts,server/src/lib/log-redaction.ts,server/src/lib/attempt-trace.ts
1. Server Logs Viewer (server-logs.ts)
User-facing guide: the dashboard viewer, its two-tier store, the polling API (
/api/logs,/api/logs/counts,/api/logs/clear), environment variables, and the React client implementation are documented inlogs/01-server-logs-viewer.md. This section covers the internals that guide deliberately omits.
The store keeps a 1000-entry in-memory ring (all levels) plus a persisted server_logs SQLite table (warn/error only) behind a single sinceId-based id space. For the operator-facing contract (tier capacities, retention, cursor semantics, request/response shape) see the Server Logs Viewer guide and its API contract.
Ingest Path
console.log/warn/error/debug/trace
│
▼
lib/log-redaction.ts (console wrapper)
├─ redacts secrets (API keys, tokens, auth headers, etc.)
▼
server-logs.ts.recordConsoleLine(level, args)
├─ formats args (Error → stack, objects → inspect depth 2)
├─ noise filter + length cap
├─ pushes to ring (splice if > 1000)
└─ if level ∈ {warn, error} → persist(entry) → INSERT INTO server_logs
Reentrancy guards:
mirroring—providerLog()mirrors to stdout via wrapped console; prevents double-ingest.persisting— DB layer can log (busy timeout, permissions); skips persist write but ring still accepts.seeding— boot seed probes DB, which can log.
Structured Provider Logs
providerLog(level, message, { provider, model, event, requestId })
- Recorded with metadata for dashboard filtering AND mirrored to stdout (redacted).
- Used by: Health checker, Cooldown probe, Router, Fallback loop, Catalog sync.
Boot Preload
On first ingest (or explicit initServerLogs() call):
- Seed
lastIdfromMAX(id)inserver_logs. - Preload latest 200 persisted rows (newest-first, reversed to oldest-first in ring).
- Re-stamp any pre-DB ring entries above persisted max (IDs unique across tiers).
Querying & Maintenance
The public query API (queryLogs({ levels, q, provider, sinceId, limit })), level-count badges (levelCounts()), and clear/reset behavior (clearLogs(), resetServerLogsForTest()) are specified in the Server Logs Viewer API contract. The ID counter is preserved across clearLogs() so a dashboard cursor never sees duplicate ids.
Desktop file logger
The desktop app has no terminal attached, so it tees every console line to a file logger (desktop/src/logger.ts, added in 90aaa5b) instead of relying on the in-memory ring: <data dir>/logs/freeapi.log, plus one rotated freeapi.log.1 at 1 MB each. The same redaction wrapper feeds both the ring (server) and the file (desktop), so secrets never reach the file either. The desktop's password-reset code appears here — see the Install & deploy FAQ.
2. Request Analytics (request-log.ts)
Per-Request Row (requests table)
| Column | Source |
|---|---|
platform, model_id, key_id |
RouteResult |
request_type |
'chat' | 'embeddings' | 'image' | 'speech' | 'transcription' | 'fusion' |
status |
'success' | 'error' | 'canceled' |
input_tokens, output_tokens, total_tokens |
Provider usage + estimates |
latency_ms |
Wall-clock |
ttfb_ms |
First token (content OR reasoning) |
error |
Redacted provider message |
created_at_ms |
Timestamp |
client_info |
User agent, IP hash |
served_model |
Upstream-reported model (observability) |
attempt_error_summary |
Aggregated failure classes for this request |
caller |
Which gateway pathway produced the request |
execution_id and the caller dimension
Every JSON body the chat completion routes send — success, cache/idempotency
replay, and error alike — carries a top-level execution_id holding the same
value as the X-Request-ID header, so a client that only kept the response body
can still find the row and its attempt trail. It is stamped on the outbound copy,
so a replayed cache hit reports the id of that request, not the one that filled
the cache. Streamed frames omit it (the header carries it there).
requests.caller records the surface: today every inference path writes http
(the OpenAI-compatible proxy, /v1/responses, /v1/messages, the Ollama and
Gemini wires, fusion sub-calls, and ask_f3router). The MCP inference tool
deliberately loops through the same local /v1/chat/completions route, so it
inherits identical routing, fallback, cache, token accounting, and error
handling instead of maintaining a second inference implementation. The other
/mcp tools are local introspection/control calls. The dashboard playground
also calls the same HTTP endpoints as any other client. The column is free-form,
so a new surface needs no migration.
Attempt Trail (request_attempts table)
One row per dispatched-and-failed attempt:
| Column | Purpose |
|---|---|
request_id |
FK to requests |
attempt_ordinal |
0-based |
platform, model_id, key_id |
What was tried |
error_class |
AttemptErrorClass (auth, rate_limited, upstream_error, ...) |
error_summary |
Redacted provider message (≤200 chars) |
latency_ms |
Per-attempt wall-clock |
ttfb_ms |
Per-attempt TTFB |
input_tokens, output_tokens |
Per-attempt estimates |
- Powers
X-Fallback-Trail/X-Fallback-Detailheaders. - Enables per-request drill-down in dashboard.
Aggregates (request_aggregates materialized view)
Pre-computed rollups for dashboard charts (24h / 7d / 30d / 90d windows):
- Latency p50 / p95 / p99
- TTFB p50 / p95
- Token throughput
- Success rate
- Estimated cost savings
- Per-provider / per-model / per-key breakdowns
3. Log Redaction (log-redaction.ts)
What Gets Redacted
| Pattern | Replacement |
|---|---|
Bearer <token> |
Bearer [REDACTED] |
x-api-key: <key> |
x-api-key: [REDACTED] |
x-goog-api-key: <key> |
x-goog-api-key: [REDACTED] |
Authorization: Basic <creds> |
Authorization: Basic [REDACTED] |
| API keys in JSON bodies | [REDACTED] |
| URLs with embedded credentials | [REDACTED] |
| SQLite encryption keys | [REDACTED] |
- Single pass at console wrapper → all log paths (console, providerLog, boot lines) are redacted.
- Idempotent: already-redacted text passes through unchanged.
4. Attempt Tracing (attempt-trace.ts)
AsyncLocalStorage Scope
Each request gets an AsyncLocalStorage<RequestTrace> scope:
interface RequestTrace {
requestId: string;
startedAtMs: number;
records: AttemptTraceRecord[];
}
interface AttemptTraceRecord {
platform: string;
modelId: string;
keyOrdinal: number; // 1-based per request
outcome: AttemptOutcome; // 'success' | 'retryable_failure' | 'auth_failure' | 'fatal' | 'hedge_abort'
startOffsetMs: number; // relative to request start
durationMs: number;
errorSummary: string | null; // redacted, ≤200 chars
}
runWithRequestTrace(fn)wraps the fallback loop.dispatch()records start offset, runs attempt, records duration + outcome + summary.getRequestTrace()reads current scope (forX-Fallback-Detailheader).
Outcomes
| Outcome | Meaning |
|---|---|
success |
Attempt completed, response sent |
retryable_failure |
429/5xx/timeout/empty → failover |
auth_failure |
401 → key revalidation triggered |
fatal |
Non-retryable (400, 404 model gone, etc.) |
hedge_abort |
Budget expired mid-attempt (1d2226a) |
5. Health Endpoint (/api/health, /livez, /readyz)
| Endpoint | Purpose |
|---|---|
GET /livez |
Process alive (k8s liveness) |
GET /readyz |
Ready to serve (DB + at least one healthy provider) |
GET /api/health |
Full status: {status, degraded, providers: [{platform, healthy, totalKeys, usableKeys, status}], catalog: {version, tier, lastSync}} |
- Degraded mode reported via
degradation.getDegradationStatus(). - Provider health from
api_keysstatuses (healthy/unknown/rate_limited/invalid/error).
6. Routing Trace Headers (Every Response)
| Header | Value |
|---|---|
X-Request-ID |
UUID (or client x-request-id) |
X-Routed-Via |
platform/model (safeHeaderValue) |
X-Fallback-Attempts |
Failed hops before success/exhaustion |
X-Fallback-Trail |
platform/model keyN=class; … (max 10, 1KB) |
X-Fallback-Detail |
Opt-in: platform/model keyN=outcome t=start+dur msg=summary; … (2KB) |
Retry-After |
Seconds until soonest cooldown expiry (429 exhaustions) |
X-FreeLLM-Cache |
HIT / MISS (response cache) |
X-FreeLLM-Compress |
Compression pipeline metadata |
7. Dashboard Analytics Pages
| Page | Data Source |
|---|---|
| Overview | request_aggregates (24h/7d/30d/90d) |
| Models | Per-model latency, tokens, success rate, cost savings |
| Providers | Per-provider breakdown + quota usage |
| Keys | Per-key health, usage, cooldowns, revalidation history |
| Logs | server_logs ring + persisted (filter by level, provider, search) |
| Cache | Hit rate, token savings, LRU stats |
8. Key Functions (server-logs.ts)
| Function | Purpose |
|---|---|
recordLogEntry(options) |
Single ingest point (console tap, providerLog, boot preload) |
recordConsoleLine(level, args) |
Called by log-redaction wrapper |
providerLog(level, message, meta) |
Structured + stdout mirror |
initServerLogs() |
Force seed/preload (route calls on first poll) |
queryLogs(query) |
Filtered, paginated, cursor-based |
levelCounts() |
Dashboard badges |
currentMaxId() |
Cursor ceiling |
clearLogs() |
Empty both tiers (ID counter preserved) |
resetServerLogsForTest() |
Test cold-start simulation |
9. Key Functions (request-log.ts)
| Function | Purpose |
|---|---|
logRequest(platform, modelId, keyId, status, inTokens, outTokens, latencyMs, error, ttfbMs, pinnedModelId) |
Insert request row |
persistRequestAttempts(requestId, attempts) |
Batch insert attempt trail |
getRequestAnalytics(window) |
Aggregated rollups for dashboard |
getRequestAttempts(requestId) |
Per-request drill-down |
10. Environment Variables
| Variable | Default | Purpose |
|---|---|---|
RESPONSE_CACHE |
0 |
Enable response cache (opt-in) |
REQUEST_MAX_TOKENS_BUDGET |
0 (off) |
Per-request token ceiling |
FALLBACK_TIME_BUDGET_MS |
45000 |
Retry budget (affects exhaustion) |
MAX_CONSECUTIVE_UPSTREAM_FAILS |
0 (off) |
Circuit breaker threshold |
EXPOSE_FALLBACK_DETAIL_HEADER |
0 |
Opt-in X-Fallback-Detail |
COOLDOWN_PROBE_DISABLED |
0 |
Kill switch for probe job |
11. Privacy & Security
- No PII in logs: Request bodies never logged. Only metadata (model, platform, tokens, latency, redacted errors).
- Redaction first: Console wrapper redacts before
server-logsever sees the line. - Single-user design: No multi-tenant log isolation needed.
- Local-first: Logs never leave the machine unless operator configures external log shipper.
12. Tables
-- Server logs (warn/error only, persists across restarts)
CREATE TABLE server_logs (
id INTEGER PRIMARY KEY,
level TEXT CHECK(level IN ('trace','debug','info','warn','error')),
source TEXT, provider TEXT, model TEXT, event TEXT, request_id TEXT,
message TEXT, created_at_ms INTEGER
);
-- Per-request analytics
CREATE TABLE requests (
id INTEGER PRIMARY KEY,
platform TEXT, model_id TEXT, key_id INTEGER,
request_type TEXT, status TEXT,
input_tokens INTEGER, output_tokens INTEGER, total_tokens INTEGER,
latency_ms INTEGER, ttfb_ms INTEGER,
error TEXT, created_at_ms INTEGER,
client_info TEXT, served_model TEXT, attempt_error_summary TEXT
);
-- Per-attempt trail
CREATE TABLE request_attempts (
id INTEGER PRIMARY KEY,
request_id INTEGER, attempt_ordinal INTEGER,
platform TEXT, model_id TEXT, key_id INTEGER,
error_class TEXT, error_summary TEXT,
latency_ms INTEGER, ttfb_ms INTEGER,
input_tokens INTEGER, output_tokens INTEGER,
FOREIGN KEY(request_id) REFERENCES requests(id)
);
-- Materialized aggregates (refreshed periodically)
CREATE VIEW request_aggregates AS ...;