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>
110 lines
3.6 KiB
Plaintext
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
|