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>
59 lines
2.7 KiB
Docker
59 lines
2.7 KiB
Docker
# Hold Slayer — single image: FastAPI process that also serves the built
|
|
# SvelteKit dashboard at "/". One container, four surfaces (REST/WS/MCP/dash).
|
|
#
|
|
# pjsua2 is deliberately NOT built here — it is not pip-installable (compiled
|
|
# from pjproject) and the media pipeline degrades to documented stub mode
|
|
# without it. That is correct for a mock-SIP dev deploy (USE_MOCK_SIP=true);
|
|
# /health honestly reports the mock engine as "degraded". Building real media
|
|
# is a separate, deliberate piece of work.
|
|
|
|
# Stage 1: build the SvelteKit dashboard → dashboard/build/ (SPA, static).
|
|
# dashboard/build and dashboard/node_modules are gitignored, so build fresh
|
|
# here rather than copying a stale working-tree artifact.
|
|
FROM node:22-alpine AS dashboard
|
|
WORKDIR /dashboard
|
|
COPY dashboard/package.json dashboard/package-lock.json ./
|
|
RUN npm ci
|
|
COPY dashboard/ ./
|
|
RUN npm run build
|
|
|
|
# Stage 2: runtime. The app runs FROM SOURCE at /app (not purely from
|
|
# site-packages): db/database.py locates alembic.ini via
|
|
# Path(__file__).parent.parent, and main.py/config.py are loose top-level
|
|
# modules — both require the source tree layout under the working dir. An
|
|
# editable install puts the deps + entry points in place while keeping /app/db,
|
|
# /app/config.py, /app/alembic.ini resolving to the real files.
|
|
FROM python:3.12-slim
|
|
WORKDIR /app
|
|
|
|
# build-essential: some deps compile from source (no manylinux wheel).
|
|
# curl: required for the compose healthcheck (GET /health).
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends build-essential curl \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Install dependencies first (better layer caching) using just the manifest,
|
|
# then the source. -e keeps the package importable from /app so alembic.ini
|
|
# and the loose modules resolve correctly at runtime.
|
|
COPY pyproject.toml README.md ./
|
|
COPY . .
|
|
# Bring in the freshly built dashboard (overwrites any stale gitignored copy).
|
|
COPY --from=dashboard /dashboard/build ./dashboard/build
|
|
|
|
RUN pip install --no-cache-dir -e . \
|
|
&& apt-get purge -y build-essential && apt-get autoremove -y
|
|
|
|
EXPOSE 21081
|
|
|
|
# Structured logs by default in the container: the host's Alloy agent reads
|
|
# stdout and ships it to Loki, where text lines arrive as an unqueryable blob.
|
|
# Overridable (LOG_FORMAT=text) for interactive `docker run` debugging.
|
|
ENV LOG_FORMAT=json
|
|
|
|
# Migrations run in the app's own init_db() on boot (db/database.py), so no
|
|
# separate `alembic upgrade` here. Bind host/port from the same env vars
|
|
# pydantic-settings reads (HOST/PORT) so configured values and the actual bind
|
|
# cannot drift. Deploy sets PORT=21081 (image default 8000 collides with other
|
|
# host-net services on triton).
|
|
CMD ["sh", "-c", "uvicorn main:app --host ${HOST:-0.0.0.0} --port ${PORT:-21081}"]
|