Files
hold-slayer/.env.example
Robert Helewka 1644999bcb feat(logging): structured JSON logs, uvicorn access log included
Hold Slayer's logs are shipped to Loki by the host's Alloy agent, which
reads container stdout. Text lines arrive there as an opaque blob:
filtering on a status code meant regex over a formatted string. This adds
LOG_FORMAT=json (default "text", so local dev stays readable) rendering
one JSON object per line.

Two parts were less obvious than a format= argument would suggest, and
both are why this is a module rather than a basicConfig tweak:

Uvicorn attaches its own handlers to `uvicorn` and `uvicorn.access` with
propagate=False, so configuring only the root logger would have left the
access log — the highest-volume, most useful stream — as colourised text
next to our JSON. configure_logging clears those handlers and re-enables
propagation, and is called both at import (for startup config checks) and
in lifespan (uvicorn configures itself after importing the app). The
__main__ path passes log_config=None so uvicorn never applies its own.

The access record's payload lives in record.args as a 5-tuple, not in the
message. Formatting it would throw the structure away and force Loki to
parse it back out, so the tuple is unpacked into real fields and
status_code is emitted as a number for range filtering.

Also drops uvicorn's `color_message` extra, an ANSI-coloured duplicate of
the message that generic extra-promotion would otherwise copy into every
startup line — the same unreadable-in-Grafana problem recently fixed for
the lab's Asterisk logs.

Verified against a real uvicorn server: 39/39 lines valid JSON, zero ANSI
escapes, no duplicates, access lines structured with correct status codes;
text mode unchanged. Thread name is included off the main thread, since
"which execution context logged this" is the first question when debugging
across the asyncio/Sippy/PJSUA2 boundary. SecretStr extras stay masked.

README Phase 4 item ticked; LOG_FORMAT and the previously-undocumented
LOG_LEVEL added to the config table and .env.example.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 06:18:06 -04:00

110 lines
3.6 KiB
Plaintext

# ============================================================
# Hold Slayer Gateway Configuration
# ============================================================
# Copy to .env and fill in your values. This is the app's own .env for a bare
# `uvicorn main:app` run — see .env.compose.example for the Docker stack.
# --- Database (required) ---
DATABASE_URL=postgresql+asyncpg://holdslayer:<db-password>@localhost:5432/holdslayer
# --- Auth: Casdoor SSO + owner-minted PATs (owner-only) ---
# The browser signs in via Casdoor (short-lived JWT); MCP/CLI clients use
# owner-minted PATs (hs_pat_…). Both resolve to a User gated to OWNER_NAME.
#
# Two supported configurations, enforced at startup:
# 1. CASDOOR_ENABLED=true + endpoint/client_id/client_secret/OWNER_NAME set
# 2. CASDOOR_ENABLED=false + HOST=127.0.0.1 (dev-owner mode, loopback ONLY)
# SSO-off with an off-loopback HOST is refused — it would resolve every request
# to the dev owner, open to the network.
CASDOOR_ENABLED=true
CASDOOR_ENDPOINT=https://id.example.com
CASDOOR_CLIENT_ID=
CASDOOR_CLIENT_SECRET=
CASDOOR_ORG_NAME=
CASDOOR_APP_NAME=hold-slayer
# The owner's Casdoor username — the only identity allowed on any surface.
OWNER_NAME=
# Public base URL the browser reaches (drives OAuth discovery + the Casdoor
# redirect_uri). Blank derives it from the request headers.
PUBLIC_BASE_URL=
# --- SIP Trunk ---
# The mock engine must be requested explicitly; an unconfigured trunk
# without USE_MOCK_SIP=true refuses to start.
USE_MOCK_SIP=false
SIP_TRUNK_HOST=sip.yourprovider.com
SIP_TRUNK_PORT=5060
SIP_TRUNK_USERNAME=your_sip_username
SIP_TRUNK_PASSWORD=your_sip_password
SIP_TRUNK_TRANSPORT=udp
# Your phone number on the trunk (E.164)
SIP_TRUNK_DID=+15551234567
# --- Gateway SIP Listener ---
# Port for devices (softphones/hardphones) to register to
GATEWAY_SIP_HOST=0.0.0.0
GATEWAY_SIP_PORT=5060
GATEWAY_SIP_DOMAIN=gateway.local
# --- Speaches STT ---
SPEACHES_URL=http://localhost:22070
SPEACHES_MODEL=whisper-large-v3
# --- Rhema TTS (OpenAI-compatible /v1/audio/speech) ---
# Must NOT point at this app's own port (default PORT=8000) — set a real
# endpoint or TTS requests loop back into the gateway.
TTS_BASE_URL=http://localhost:8001
TTS_MODEL=speaches-ai/Kokoro-82M-v1.0-ONNX
TTS_VOICE=af_heart
TTS_API_KEY=
TTS_TIMEOUT=30.0
TTS_SAMPLE_RATE=16000
# --- Audio Classifier ---
# Thresholds for hold music detection (0.0 - 1.0)
CLASSIFIER_MUSIC_THRESHOLD=0.7
CLASSIFIER_SPEECH_THRESHOLD=0.6
CLASSIFIER_SILENCE_THRESHOLD=0.85
# Analysis window in seconds
CLASSIFIER_WINDOW_SECONDS=3.0
# --- LLM (OpenAI-compatible API) ---
# Ollama, LM Studio, vLLM, or OpenAI — any OpenAI-compatible endpoint
LLM_BASE_URL=http://localhost:11434/v1
LLM_MODEL=llama3
LLM_API_KEY=not-needed
LLM_TIMEOUT=30.0
LLM_MAX_TOKENS=1024
LLM_TEMPERATURE=0.3
# --- AI Receptionist (inbound calls) ---
RECEPTIONIST_ENABLED=true
RECEPTIONIST_LISTEN_TIMEOUT_S=15.0
RECEPTIONIST_END_OF_UTTERANCE_SILENCE_S=1.2
RECEPTIONIST_MESSAGE_MAX_SECONDS=90
# --- Hold Slayer ---
# Default device to transfer to when human detected
DEFAULT_TRANSFER_DEVICE=sip_phone
# Max hold time before giving up (seconds)
MAX_HOLD_TIME=7200
# How often to check classification while on hold (seconds)
HOLD_CHECK_INTERVAL=2.0
# --- Notifications ---
# SMS notification number (optional)
NOTIFY_SMS_NUMBER=+15559876543
# --- Server ---
HOST=0.0.0.0
PORT=8000
DEBUG=false
LOG_LEVEL=info
# Log rendering: "text" (human-readable) or "json" (one object per line, for
# Loki/Alloy). The Docker image sets json; text is the default for local dev.
LOG_FORMAT=text
# --- Safety ---
# Max simultaneous calls the gateway will place (REST + MCP)
MAX_CONCURRENT_CALLS=4