fix
This commit is contained in:
@@ -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}"'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user