-
Notifications
You must be signed in to change notification settings - Fork 3
Request Logging
Every request that passes through the proxy is logged with detailed telemetry. This helps you understand performance, identify bottlenecks, and debug issues.
Important
The proxy records metadata only - never the prompt content or response text.
All fields are written to the request_logs PostgreSQL table.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key (auto-generated via uuid.New()) |
request_hash |
TEXT | Random 16-character hex request identifier. Generated with crypto/rand (8 random bytes β 16 hex characters). Not content-derived. |
model_id |
TEXT | The requested model (e.g. deepseek/deepseek-chat or hotel/gpt-4) |
provider_id |
UUID | Which provider handled the request (NULL until resolved, NULL if provider deleted) |
virtual_key_id |
UUID | Foreign key to virtual_keys table (NULL for anonymous requests) |
virtual_key_name |
TEXT | Which virtual key was used. Retained even after key deletion for audit purposes. |
status_code |
INT | HTTP status for the request, updated across its lifecycle (nullable, no default). The initial insert omits it (NULL); once upstream headers arrive the row is updated with the upstream status β e.g. 200 β and moves to state='streaming', so a long-lived stream reads 200 while still in flight. A mid-stream failure is then rewritten to 0 (its classification, when one is assigned, lives in error_kind). On a non-streaming failure the final value is path-dependent: a terminal upstream non-2xx keeps the upstream's own status (e.g. 500, 429, 401); only when all candidates are exhausted without a forwardable response does the proxy synthesise a truthful gateway code β 499 (client hangup), 504 (failover/retry timeout), or 502 (provider/transport failure). A request that never received upstream headers stays NULL. See Error classification. |
error_message |
TEXT | Error text on failure. Populated for upstream provider errors and proxy-internal errors (invalid model format, provider not found, etc.). Empty on success. |
error_kind |
TEXT | Machine-readable failure classification (added in migration 045), set alongside error_message. One of client_disconnect, provider_error, provider_timeout, failover_timeout, retry_timeout, internal, validation, auth. NULL on success, for rows predating migration 045 (no backfill), and for a few edge paths that record only an error_message (e.g. a stream truncated before the [DONE] sentinel). The dashboard's "Interrupted" badge reads this, falling back to substring-matching the English error_message only when it is NULL. The Prometheus error_kind label carries the raw value verbatim and is empty ("") when unset β it does not fall back, so legacy/truncated rows with no kind appear unclassified in metrics. |
streaming |
BOOLEAN | Whether the request used SSE streaming |
failover_attempt |
INT | Which attempt this was - 0 for the first try, incrementing with each failover |
resolved_model_id |
TEXT | The actual upstream model ID used for the request (e.g. openai/gpt-4o). For hotel-routed requests, this differs from model_id which contains the requested hotel/ name (e.g. hotel/gpt-4o). NULL for non-failover requests where the requested and resolved model are the same. |
state |
TEXT | Request lifecycle state: pending β streaming β completed or failed
|
endpoint_type |
TEXT | Endpoint family the request came through: chat (default), embeddings, image, tts, or stt. Written at INSERT time; exposed in the Logs API response, filterable via ?endpoint_type=, and shown as a badge in the dashboard Logs page for non-chat requests. |
duration_ms |
DOUBLE PRECISION | End-to-end wall time (request start to response end) |
latency_ms |
DOUBLE PRECISION | Provider response time only (duration_ms - proxy_overhead_ms) |
proxy_overhead_ms |
DOUBLE PRECISION | Total proxy overhead (sum of all seven overhead phases) |
parse_ms |
DOUBLE PRECISION | JSON parsing and request validation time |
failover_lookup_ms |
DOUBLE PRECISION | Time resolving the failover group for hotel/ requests (0 for direct requests) |
model_lookup_ms |
DOUBLE PRECISION | Time resolving the model entity, checking enabled status, building candidate list |
provider_lookup_ms |
DOUBLE PRECISION | Time finding provider record in database (excludes key decryption) |
key_decrypt_ms |
DOUBLE PRECISION | Time decrypting provider API key (first call per 10-minute cache window; subsequent calls near-zero) |
settings_read_ms |
DOUBLE PRECISION | Time reading runtime settings from cache (circuit breaker state, rate limits, etc.) |
dial_ms |
DOUBLE PRECISION | Full DNS + TCP dial time to the upstream provider (renamed from safe_dial_ms in migration 032) |
cache_hits |
JSONB | Per-component cache hit/miss flags (failover, model, provider, key, settings); true = prewarmed cache hit, false = miss, absent = not applicable |
response_header_ms |
REAL | Time until upstream HTTP response headers arrived (renamed from the old ttft_ms in migration 035, which had actually measured headers, not tokens) |
ttft_ms |
REAL | True time to first token (streaming requests only). Measured by the TTFT probe that reads ahead to confirm the first data chunk before committing the stream. |
tokens_per_second |
DOUBLE PRECISION | Streaming throughput (completion_tokens / total_duration Γ 1000) |
tokens_prompt |
INT | Number of prompt tokens reported by the provider |
tokens_completion |
INT | Number of completion tokens reported by the provider |
tokens_completion_reasoning |
INT | Reasoning tokens (DeepSeek-R1, etc.). Written to DB and exposed in Logs API. |
tokens_prompt_cache_hit |
INT | Prompt cache hit tokens (DeepSeek). Written to DB and exposed in the Logs API response. |
tokens_prompt_cache_miss |
INT | Prompt cache miss tokens (DeepSeek). Written to DB and exposed in the Logs API response. |
created_at |
TIMESTAMPTZ | When the request was inserted (defaults to now()) |
The proxy never logs prompt text, completion text, or any user content. The request_hash is a random identifier generated with crypto/rand (8 random bytes β 16 hex characters) - it is not derived from request content in any way.
The request_id TEXT column (from the initial schema) was never populated by the proxy and was always empty in the Logs API response. It was dropped in migration 030_drop_request_id.sql.
β οΈ Thepromptcolumn was removed in migration 027. It was added in migration 006 but no application code ever wrote to it. The column has been dropped entirely - it no longer exists in the database schema.
The proxy_overhead_ms field is decomposed into seven phases, measured in parallel with the actual provider request:
| Phase | DB Column | Description |
|---|---|---|
| Parse | parse_ms |
JSON parsing and request validation |
| Failover lookup | failover_lookup_ms |
Resolving the failover group for hotel/ requests (0 for direct provider requests) |
| Model lookup | model_lookup_ms |
Resolving the model entity, checking model and provider enabled status, and building the ordered candidate list |
| Provider lookup | provider_lookup_ms |
Finding the provider record in the database (excludes key decryption time) |
| Key decryption | key_decrypt_ms |
Decrypting the provider API key (first call per 10-minute cache window; subsequent calls within the window are near-zero) |
| Settings read | settings_read_ms |
Time spent reading runtime settings from the cached settings store (circuit breaker state, rate limits, etc.) |
| Dial | dial_ms |
Full DNS + TCP dial time to the upstream provider |
The companion cache_hits JSONB column records, per component (failover, model, provider, key, settings), whether the lookup hit a prewarmed cache - useful for explaining why the same phase is sometimes fast and sometimes slow.
These represent pure proxy overhead - the time spent inside Model Hotel before and after the upstream call. You can use this to determine whether latency is coming from the provider or from the proxy itself.
The relationship between fields is:
proxy_overhead_ms = parse_ms + failover_lookup_ms + model_lookup_ms + provider_lookup_ms + key_decrypt_ms + settings_read_ms + dial_mslatency_ms = duration_ms - proxy_overhead_ms
| State | Description | Transitions |
|---|---|---|
pending |
Initial state when request is received | β streaming (on headers) or β failed (on error) |
streaming |
SSE stream in progress (headers sent, tokens flowing) | β completed (on success) or β failed (on error/interrupt) |
completed |
Request finished successfully (2xx status) | Terminal state |
failed |
Request failed (4xx/5xx status, timeout, or stale cleanup) | Terminal state |
When a request arrives, insertRequestLogAsync() is called:
func (h *Handler) insertRequestLogAsync(logEntry *requestLogData) {
logEntry.id = uuid.New().String()
logEntry.requestHash = generateRequestHash()
// Async INSERT with minimal fields
INSERT INTO request_logs (id, model_id, request_hash, streaming, virtual_key_name, virtual_key_id, failover_attempt, state)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
}Fields written at this stage:
-
id- UUID generated synchronously -
request_hash- Random 16-char hex ID -
model_id- From request body -
streaming- Boolean from request -
virtual_key_name- From context (if authenticated) -
virtual_key_id- From context (if authenticated) -
failover_attempt-0initially -
state-"pending"
All other fields are NULL at this point. The INSERT runs in a goroutine with a 5-second timeout.
When upstream headers arrive (streaming) or the full response is received (non-streaming), updateRequestLog() is called:
func (h *Handler) updateRequestLog(ctx context.Context, logEntry *requestLogData) {
h.WaitForInsert(logEntry) // Block until async INSERT completes
logEntry.latencyMs = logEntry.durationMs - logEntry.proxyOverheadMs
UPDATE request_logs SET
provider_id = $2,
status_code = $3,
duration_ms = $4,
latency_ms = $19,
proxy_overhead_ms = $5,
parse_ms = $6,
model_lookup_ms = $7,
provider_lookup_ms = $8,
key_decrypt_ms = $9,
dial_ms = $20,
settings_read_ms = $21,
ttft_ms = $10,
tokens_per_second = $11,
tokens_prompt = $12,
tokens_completion = $13,
tokens_prompt_cache_hit = $14,
tokens_prompt_cache_miss = $15,
error_message = $16,
failover_attempt = $17,
state = $18
WHERE id = $1
}For streaming requests:
- State transitions:
pendingβstreaming(headers received) βcompleted/failed(stream ends) - The same
updateRequestLog()is called twice: once when headers arrive, once when the stream completes
For non-streaming requests:
- State transitions:
pendingβcompleted/failed(single UPDATE)
The failRequest() helper populates error details:
func (h *Handler) failRequest(ctx context.Context, logData *requestLogData, statusCode int, errMsg string, ...) {
logData.statusCode = statusCode
logData.errorMessage = errMsg
logData.durationMs = float64(time.Since(startTime).Microseconds()) / 1000.0
logData.state = "failed"
h.updateRequestLog(ctx, logData)
}Error messages are truncated to 200 characters (with β¦ suffix) for SSE events.
Request lifecycle events are published to the event bus:
| Event Type | Severity | When | Metadata |
|---|---|---|---|
request.started |
info |
On initial INSERT |
request_id, model_id, streaming, state
|
request.completed |
success |
On successful completion |
request_id, model_id, state, status_code
|
request.completed |
warning |
On failure |
request_id, model_id, state, status_code, error_message (truncated) |
The log_retention setting controls how long request logs are kept. When set, a background goroutine purges logs older than the retention period every hour.
Accepted values: 1h, 1d (or 24h), 1w (or 168h), 1m (or 720h). Set to 0 or leave empty to disable automatic cleanup.
This setting can be changed at runtime via the Settings API (PUT /api/settings).
The stale_request_timeout setting (default: 30m) controls how long a request can remain in pending or streaming state before being marked as failed with the error "request interrupted (stale)".
Cleanup runs every 5 minutes and applies two strategies:
-
Server-start-time check: Any in-progress row that predates the current server process is definitively orphaned (the previous process is dead). This has zero false-positive risk.
-
Age-based check: Rows older than
stale_request_timeoutare marked failed. This catches in-process orphans (e.g., a panic skips the finalupdateRequestLog). The timeout is generous to avoid killing legitimate long-running streaming requests.
Accepted values: 5m, 10m, 15m, 30m (default), 1h, 2h, 0s (disabled).
Stale cleanup also runs on server startup, marking any in-progress rows that predate the current process as failed.
API endpoint: DELETE /api/logs/purge
Request body:
{
"older_than": "1h"
}Accepted values: 1h, 1d, 1w, 1m, all
Response: 204 No Content on success.
The dashboard has two log views, accessible from the Logs sidebar entry with sub-mode toggling.
API endpoint: GET /api/logs
Shows all proxy requests with:
- Sortable columns (timestamp, model, provider, status, latency, TTFT, tokens, etc.)
- Filters (date range, model, provider, status code, virtual key)
- Pagination (default 20 per page, max 200)
- In-flight request highlighting (rows pulse with animation while streaming)
- Click-through to provider/model details
- Status code color coding (green for 2xx, red for 4xx/5xx)

API endpoint: GET /api/logs/app
Shows the server's own application logs:
- Persisted to the
app_logsdatabase table - Async batch writer for performance (buffers writes, flushes every 500ms or every 50 entries)
- Severity levels (
info,warning,error) - inferred from log content heuristics - Source attribution (which package/module produced the log, parsed from
[prefix]tags) - Two access modes:
-
Ring buffer mode (default) - returns the last N entries from an in-memory buffer of 500, with optional
?after=polling for live updates -
History mode (
?history=true) - queries the database with full filtering, pagination, and sorting
-
Ring buffer mode (default) - returns the last N entries from an in-memory buffer of 500, with optional

By default the server logs at Info and above. To get the per-request Debug mechanics:
-
DEBUG_LOG=trueturns Debug on for every scope. Thorough, but it floods both stdout and this App Logs view at any real request rate. -
DEBUG_LOG_SCOPES=failover,ratelimitturns Debug on for only the named scopes - far more usable in production. It is ignored whenDEBUG_LOG=true.
A scope is the source prefix before the first : in a log line (matched
case-insensitively). The canonical scopes are:
proxy, resolve, discovery, failover, provider, settings, backup,
webauthn, stats, system, db, admin, applogs, events, ratelimit,
keycache, docker, auth, model, virtual-keys, version, api.
Both variables are startup-only (env vars, set via .env / compose). See
Configuration for the full table.
| Column | Type | Description |
|---|---|---|
id |
UUID PK | Auto-generated |
timestamp |
TIMESTAMPTZ NOT NULL | When the log was emitted |
level |
TEXT NOT NULL | Severity: info, warning, error
|
source |
TEXT NOT NULL | Package/module that emitted the log |
message |
TEXT NOT NULL | Log message content |
created_at |
TIMESTAMPTZ NOT NULL | DB insertion timestamp (default now()) |
| Parameter | Description |
|---|---|
page |
Page number (default 1) |
per_page |
Results per page (1β200, default 20) |
model_id |
Filter by model ID (ILIKE match) |
provider_id |
Filter by provider UUID |
status_code |
Filter by HTTP status. Accepts exact codes, 4xx, 5xx, or 0 for in-progress/never-responded |
endpoint_type |
Filter by endpoint family: chat, embeddings, image, tts, stt (unknown values are ignored) |
from |
Start timestamp (RFC3339) |
to |
End timestamp (RFC3339) |
sort_by |
Sort column: time, model, provider, status, tokens, tps, ttft, duration, overhead, key
|
sort_dir |
asc or desc (default desc) |
Special sorting behavior:
-
provider: NULL providers sort last, deleted providers (name NULL but ID present) sort second-to-last -
status: Errors with cancel/disconnect/context-canceled messages sort after other errors -
key: Deleted virtual keys (ID present but no matching row) sort last
| Parameter | Description |
|---|---|
level |
Filter by severity: info, warning, error
|
source |
Filter by log source (e.g. proxy, auth, discovery) |
search |
Text search in message (ILIKE match) |
from |
Start timestamp (RFC3339) |
to |
End timestamp (RFC3339) |
page |
Page number (default 1) |
per_page |
Results per page (1β100, default 20) |
sort_by |
Sort column: time, level, source, message
|
sort_dir |
asc or desc (default desc) |
History mode returns a structured response including entries, total, page, per_page, plus level_counts and source_counts aggregates (cached for 5 seconds).
Streaming requests are logged in three phases:
A log entry is created with state=pending. The initial INSERT (async) writes:
model_idrequest_hashstreaming=truevirtual_key_namevirtual_key_idfailover_attempt=0state=pending
All other fields are NULL at this point.
The entry is updated with:
state=streaming-
provider_id(resolved provider) -
status_code(from upstream) -
proxy_overhead_msand all breakdown fields (parse_ms,model_lookup_ms, etc.) -
ttft_ms(time to first token) -
resolved_model_id(actual upstream model ID, differs frommodel_idfor hotel-routed requests)
The same entry is updated with final status:
-
state=completedorstate=failed -
duration_ms(total wall time) -
latency_ms(provider time only) -
tokens_prompt,tokens_completion -
tokens_per_second(throughput) -
error_message(if failed)
This means you can see in-flight requests in the log table in real-time. They appear with a pulsing row animation and update as the stream completes. A request that is interrupted (server crash, client disconnect) will remain in pending or streaming state until the stale cleanup goroutine marks it as failed.
The model_id field is extracted directly from the request body's model field:
var req ChatCompletionRequest
json.Unmarshal(bodyBytes, &req) // req.Model β logData.modelIDFor failover group requests (hotel/xxx), the display model name is logged, not the resolved provider-specific model.
The provider_id is populated when the upstream connection is established:
logData.providerID = candidate.provider.ID // After resolveHotelModel() succeedsIf the provider is later deleted, the provider_id remains (foreign key with no CASCADE), but the JOIN in the Logs API returns 'Deleted' as the provider name.
Token counts are extracted from the upstream response:
Non-streaming:
var upstreamResp ChatCompletionResponse
json.Unmarshal(body, &upstreamResp)
logData.tokensPrompt = upstreamResp.Usage.PromptTokens
logData.tokensCompletion = upstreamResp.Usage.CompletionTokensStreaming: Tokens are accumulated as SSE chunks arrive:
for scanner.Scan() {
// Parse data: {"usage": {"prompt_tokens": 10, "completion_tokens": 20}}
if chunk.Usage.PromptTokens > 0 {
promptTokens = chunk.Usage.PromptTokens
}
if chunk.Usage.CompletionTokens > 0 {
completionTokens = chunk.Usage.CompletionTokens
}
}
logData.tokensPrompt = promptTokens
logData.tokensCompletion = completionTokensSome providers send token counts only in the final chunk; others send them in every chunk. The proxy uses the last non-zero value.
DeepSeek providers may report prompt_cache_hit_tokens and prompt_cache_miss_tokens in the usage object. These are captured, written to the tokens_prompt_cache_hit and tokens_prompt_cache_miss columns, and exposed in the Logs API response.
Most failed requests are tagged with a machine-readable error_kind, defined in internal/proxy/reqerror.go. The persisted status_code is not a fixed function of error_kind β it depends on where the request failed (see the status_code column and the notes below the table):
error_kind |
Status code | When | Example error_message
|
|---|---|---|---|
validation |
400 / 404 | Malformed body, missing model, invalid model format (400); unknown model / no enabled providers (404) |
"invalid request body", "model 'xxx' not found"
|
auth |
403 | Virtual key lacks access to any provider for the model | "virtual key does not have access to any provider for this model" |
provider_error |
upstream status, else 502 | A terminal upstream non-2xx is logged with that status (e.g. 500, 429, 401); a transport failure or all-candidates-exhausted with no forwardable response is 502
|
"all 3 providers failed; last error: provider \"X\" returned HTTP 500 on attempt 2" |
provider_timeout |
502 | TTFT probe or stall watchdog fired β provider connected but produced no output in time | "provider \"X\" did not return a response in time on attempt 1" |
failover_timeout |
504 | The overall failover deadline expired | "request timed out while waiting on provider \"X\"" |
retry_timeout |
504 | The param-strip retry's deadline expired | "retry without unsupported parameters timed out on provider \"X\"" |
client_disconnect |
499 | The calling client hung up before we responded | "client disconnected during attempt 1 to provider \"X\"" |
internal |
502 | Gateway-internal failure (e.g. could not build the upstream request) | "internal error on attempt 1: β¦" |
The
Status codecolumn is the code written for non-streaming failures.validationandauthrows always carry the code shown (they are rejected before any upstream attempt), as does the all-candidates-exhausted path. Two cases diverge:
- A terminal upstream non-2xx (non-streaming) is logged with the upstream's own status (e.g.
500,429,401), not502β the gateway only synthesises502/504/499once every candidate is exhausted without a forwardable response.- Any mid-stream failure is logged with
status_code = 0regardless oferror_kind. Find these bystate = 'failed'(anderror_kind), not by status.
Two background states are written by the stale-log sweep in cmd/server/main.go (not a live request). The sweep updates only state, error_kind, and error_message β it does not touch status_code, so a still-pending row keeps its NULL/0, while a row already in the streaming state retains its interim status (typically 200):
| Category | status_code |
error_kind |
error_message |
|---|---|---|---|
| Stale timeout | unchanged (NULL/0, or 200 if streaming) | internal |
"request interrupted (stale)" |
| Server restart | unchanged (NULL/0, or 200 if streaming) | internal |
"request interrupted (server restart)" |
Historical rows predating migration
045have a NULLerror_kind; the dashboard falls back to substring-matching theirerror_messageto render the "Interrupted" badge.
For SSE events, error messages are truncated to 200 characters with a β¦ suffix:
For failover non-200 responses, the upstream error body is truncated to 2000 characters (with β¦ suffix) before logging and forwarding to the client. This prevents excessively large error responses from consuming memory or log space.
msg = fmt.Sprintf("Request failed: %s - %s", logEntry.modelID, logEntry.errorMessage)
if len(msg) > 200 {
msg = msg[:200] + "β¦"
}The full error message is stored in the database without truncation.
Client disconnects are detected via context cancellation:
select {
case <-r.Context().Done():
clientDisconnected = true
goto logUpdate
}The log entry is updated with state=failed and an appropriate error message.
The request_logs table has evolved through these migrations:
| Migration | Changes |
|---|---|
001_init.sql |
Initial schema: id, provider_id, model_id, status_code, latency_ms, tokens_prompt, tokens_completion, streaming, error_message, created_at
|
006_enhanced_logs.sql |
Added: request_hash, ttft_ms, proxy_overhead_ms, duration_ms, tokens_per_second, virtual_key_name, prompt (never used) |
007_overhead_breakdown.sql |
Added: parse_ms, model_lookup_ms, provider_lookup_ms, key_decrypt_ms
|
008_timing_precision.sql |
Converted timing columns from INT to REAL for sub-ms precision |
011_failover_attempt.sql |
Added: failover_attempt
|
012_add_virtual_key_id_to_request_logs.sql |
Added: virtual_key_id
|
013_backfill_virtual_key_id.sql |
Backfilled virtual_key_id for existing logs with matching virtual_key_name
|
020_log_state_column.sql |
Added: state with default 'pending', backfilled existing rows |
024_cleanup_stale_logs.sql |
One-time cleanup of rows stuck in pending/streaming state |
027_drop_unused_prompt_column.sql |
Dropped prompt column (never written to) |
028_add_timing_columns.sql |
Added: safe_dial_ms, settings_read_ms (DOUBLE PRECISION) |
030_drop_request_id.sql |
Dropped request_id column (never populated) |
031_reasoning_tokens.sql |
Added: tokens_completion_reasoning (reasoning/thinking models report these separately) |
032_rename_dial_add_failover_lookup.sql |
Renamed safe_dial_ms β dial_ms (now full DNS+TCP dial); added failover_lookup_ms
|
035_rename_ttft_to_response_header_ms.sql |
Renamed old ttft_ms β response_header_ms (it measured time-to-headers); added new true ttft_ms
|
036_resolved_model_id.sql |
Added: resolved_model_id (actual model that served a hotel/ request) |
042_cache_hits.sql |
Added: cache_hits JSONB (per-component cache hit/miss flags) |
043_endpoint_type.sql |
Added: endpoint_type with default 'chat' (multimodal proxy endpoints: embeddings, image, tts, stt) |
045_error_kind.sql |
Added: error_kind (nullable machine-readable failure classification; no backfill) |
The log ID is generated synchronously, but the INSERT runs asynchronously:
func (h *Handler) insertRequestLogAsync(logEntry *requestLogData) {
logEntry.id = uuid.New().String() // Synchronous
logEntry.requestHash = generateRequestHash() // Synchronous
logEntry.insertWg.Add(1)
go func() {
defer logEntry.insertWg.Done()
// Async INSERT with 5-second timeout
}()
}This ensures the ID is available for the subsequent UPDATE without blocking the request.
Before any UPDATE, WaitForInsert() blocks until the async INSERT completes:
func (h *Handler) WaitForInsert(logEntry *requestLogData) {
done := make(chan struct{})
go func() {
defer close(done)
logEntry.insertWg.Wait()
}()
select {
case <-done:
case <-time.After(5 * time.Second):
debuglog.Warn("proxy: timed out waiting for request log INSERT")
}
}This prevents race conditions where the UPDATE runs before the INSERT.
The Logs API caches query results with a short TTL to reduce database load during live polling:
var globalLogsCache = &logsCache{
data: make(map[string]*LogsResponse),
ttl: 2 * time.Second,
}Cache hits include X-Cache: HIT header; misses include X-Cache: MISS.
- App Logs - Application log system (ring buffer + DB, documented below)
-
Configuration - Runtime configuration including
log_retentionandstale_request_timeout - API Reference - SSE event system for real-time updates
Last synced from hugalafutro/model-hotel@3b28720 on 2026-07-08 20:04 UTC. Edit these pages under wiki/ (and images under docs/screenshots/) in the main repo, not here.
- π¨ Home
- βοΈ Configuration
- π Development
- π Virtual Keys
- π API Reference
- π Request Logging
- π Model Discovery
- π Failover and Hotel Routing
- π Alerting
- π¨ High Availability