This commit is contained in:
Mikan
2026-06-19 16:48:07 +03:00
parent 5a78def096
commit a69189c85b
4 changed files with 63 additions and 8 deletions

View File

@@ -19,5 +19,9 @@ COPY . .
EXPOSE 8000 EXPOSE 8000
# Default: run uvicorn with hot reload for dev # Default: run uvicorn with hot reload for dev.
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] # We pass --log-level explicitly from $LOG_LEVEL so uvicorn's own loggers
# (uvicorn, uvicorn.access) start at the right level from the very first
# request — without this, they stay at INFO until our lifespan runs.
# Shell form (not exec form) so ${LOG_LEVEL} is interpolated by the shell.
CMD sh -c 'exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload --log-level "${LOG_LEVEL:-info}"'

View File

@@ -1,4 +1,18 @@
"""Structured logging setup.""" """Structured logging setup.
`LOG_LEVEL` (env) controls the verbosity of:
- The root Python logger
- structlog-bound loggers (app.*)
- Uvicorn's own loggers (`uvicorn`, `uvicorn.access`, `uvicorn.error`,
`uvicorn.asgi`) — these otherwise stay at INFO regardless of LOG_LEVEL
because uvicorn configures them itself at startup, before our lifespan
calls setup_logging(). We forcibly re-level them here.
- The `sqlalchemy.engine` logger (kept at WARNING unless LOG_LEVEL=DEBUG).
Note: `logging.basicConfig()` is a no-op once the root logger has been
configured (which uvicorn does at import time), so it alone is NOT enough
to honor LOG_LEVEL — we must also call `setLevel()` on each named logger.
"""
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -9,15 +23,46 @@ import structlog
from app.config import settings from app.config import settings
# Loggers whose level must be forced to LOG_LEVEL (uvicorn pre-configures them
# at INFO before our lifespan runs, so basicConfig cannot change them).
_FORCED_LOGGERS = (
"uvicorn",
"uvicorn.access",
"uvicorn.error",
"uvicorn.asgi",
"fastapi",
)
def setup_logging() -> None: def setup_logging() -> None:
level = getattr(logging, settings.log_level.upper(), logging.INFO) level = getattr(logging, settings.log_level.upper(), logging.INFO)
# Force the root logger level (affects any logger that doesn't override).
logging.getLogger().setLevel(level)
# Also call basicConfig for the formatter (idempotent if already set up).
logging.basicConfig( logging.basicConfig(
format="%(message)s", format="%(message)s",
stream=sys.stdout, stream=sys.stdout,
level=level, level=level,
force=True, # python 3.8+: re-init even if already configured
) )
# Force level on loggers that uvicorn pre-configured.
for name in _FORCED_LOGGERS:
lg = logging.getLogger(name)
lg.setLevel(level)
# Ensure uvicorn access logs propagate to the root handler.
lg.propagate = True
for h in lg.handlers:
h.setLevel(level)
# SQLAlchemy is chatty at INFO; keep it at WARNING unless explicitly DEBUG.
sa_level = logging.DEBUG if level <= logging.DEBUG else logging.WARNING
logging.getLogger("sqlalchemy.engine").setLevel(sa_level)
logging.getLogger("sqlalchemy.pool").setLevel(sa_level)
logging.getLogger("asyncpg").setLevel(sa_level)
structlog.configure( structlog.configure(
processors=[ processors=[
structlog.contextvars.merge_contextvars, structlog.contextvars.merge_contextvars,

View File

@@ -11,12 +11,18 @@ from app.api import admin, auth, misc, presets, sessions, worlds
from app.config import settings from app.config import settings
from app.logging_setup import get_logger, setup_logging from app.logging_setup import get_logger, setup_logging
# Configure logging as early as possible — at import time, before uvicorn
# finishes its own logger setup. This ensures LOG_LEVEL is honored for the
# very first request and for startup messages from submodules.
setup_logging()
log = get_logger("app")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
# Re-apply logging config in case any submodule reset it during import.
setup_logging() setup_logging()
log = get_logger("app") log.info("app_starting", worker_mode=settings.is_worker, log_level=settings.log_level)
log.info("app_starting", worker_mode=settings.is_worker)
# Initialize DB tables and seed defaults # Initialize DB tables and seed defaults
from app.migrations.init_db import init_db from app.migrations.init_db import init_db

View File

@@ -28,10 +28,10 @@ When calling `submit_world_definition`:
- `setting_description`: 1-2 paragraph expanded setting. - `setting_description`: 1-2 paragraph expanded setting.
- `rules`: object with keys like `stats`, `combat`, `magic`, `time`, `inventory`, `death` (whichever apply). - `rules`: object with keys like `stats`, `combat`, `magic`, `time`, `inventory`, `death` (whichever apply).
- `world_schema`: a JSON Schema describing the shape of the world's state (player, npcs, locations, world_time, flags, etc.). - `world_schema`: a JSON Schema describing the shape of the world's state (player, npcs, locations, world_time, flags, etc.).
- `plot_rails`: `{main_goal, subgoals, hooks}`. - `plot_rails`: an object with keys `main_goal` (string), `subgoals` (array of strings), and `hooks` (array of strings).
- `initial_state`: the initial world state matching `world_schema`. - `initial_state`: the initial world state matching `world_schema`.
- `initial_time`: world-time string in the form `day_N_hour_H` (e.g. `day_1_hour_8`). - `initial_time`: world-time string in the form `day_N_hour_H` (e.g. `day_1_hour_8`).
- `calendar`: optional. `{hours_per_day: 24, minutes_per_hour: 60, days_per_week: 7}`. Include only if the world uses a non-standard calendar (e.g. 28-hour days). - `calendar`: optional. An object with `hours_per_day` (default 24), `minutes_per_hour` (default 60), `days_per_week` (default 7). Include only if the world uses a non-standard calendar (e.g. 28-hour days).
- `is_final`: set to `true` ONLY when the player has explicitly accepted the world. - `is_final`: set to `true` ONLY when the player has explicitly accepted the world.
CRITICAL: CRITICAL:
@@ -100,7 +100,7 @@ SUMMARIZER_SYSTEM = """You compress the history of a role-playing session. Given
Call the `submit_summary` tool with: Call the `submit_summary` tool with:
- `summary`: 3-6 sentences of key events and state changes (max 150 words). - `summary`: 3-6 sentences of key events and state changes (max 150 words).
- `facts`: array of important persistent facts `[{kind, name, description}]` where kind is one of npc, location, item, lore, event. - `facts`: array of important persistent facts. Each fact is an object with keys `kind`, `name`, `description`, where `kind` is one of `npc`, `location`, `item`, `lore`, `event`.
CRITICAL: Preserve names, numbers, and important state changes. Your text response is ignored — only the `submit_summary` tool call is used. CRITICAL: Preserve names, numbers, and important state changes. Your text response is ignored — only the `submit_summary` tool call is used.
""" """