diff --git a/.env.example b/.env.example index a7ae7af..40451f7 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,24 @@ DEBUG=false # Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL LOG_LEVEL=INFO +# Per-module log level overrides (optional) +# These override LOG_LEVEL for specific modules. +# LOG_LEVEL_GAME=DEBUG # Core game logic +# LOG_LEVEL_AI=DEBUG # AI decisions (very verbose at DEBUG) +# LOG_LEVEL_HANDLERS=DEBUG # WebSocket message handlers +# LOG_LEVEL_ROOM=DEBUG # Room/lobby management +# LOG_LEVEL_AUTH=DEBUG # Auth stack (auth, routers.auth, services.auth_service) +# LOG_LEVEL_STORES=DEBUG # Database/Redis operations + +# --- Preset examples --- +# Staging (debug game logic, quiet everything else): +# LOG_LEVEL=INFO +# LOG_LEVEL_GAME=DEBUG +# LOG_LEVEL_AI=DEBUG +# +# Production (minimal logging): +# LOG_LEVEL=WARNING + # Environment name (development, staging, production) ENVIRONMENT=development diff --git a/docs/v3/V3_18_POSTGRES_STORAGE_EFFICIENCY.md b/docs/v3/V3_18_POSTGRES_STORAGE_EFFICIENCY.md new file mode 100644 index 0000000..99b99d6 --- /dev/null +++ b/docs/v3/V3_18_POSTGRES_STORAGE_EFFICIENCY.md @@ -0,0 +1,57 @@ +# V3.18: PostgreSQL Game Data Storage Efficiency + +**Status:** Planning +**Priority:** Medium +**Category:** Infrastructure / Performance + +## Problem + +Per-move game logging stores full `hand_state` and `visible_opponents` JSONB on every move. For a typical 6-player, 9-hole game this generates significant redundant data since most of each player's hand doesn't change between moves. + +## Areas to Investigate + +### 1. Delta Encoding for Move Data + +Store only what changed from the previous move instead of full state snapshots. + +- First move of each round stores full state (baseline) +- Subsequent moves store only changed positions (e.g., `{"player_0": {"pos_2": "5H"}}`) +- Replay reconstruction applies deltas sequentially +- Trade-off: simpler queries vs. storage savings + +### 2. PostgreSQL TOAST and Compression + +- TOAST already compresses large JSONB values automatically +- Measure actual on-disk size vs. logical size for typical game data +- Consider whether explicit compression (e.g., storing gzipped blobs) adds meaningful savings over TOAST + +### 3. Retention Policy + +- Archive completed games older than N days to a separate table or cold storage +- Configurable retention period via env var (e.g., `GAME_LOG_RETENTION_DAYS`) +- Keep aggregate stats even after pruning raw move data + +### 4. Move Logging Toggle + +- Env var `GAME_LOGGING_ENABLED=true|false` to disable move-level logging entirely +- Useful for non-analysis environments (dev, load testing) +- Game outcomes and stats would still be recorded + +### 5. Batch Inserts + +- Buffer moves in memory and flush periodically instead of per-move INSERT +- Reduces database round-trips during active games +- Risk: data loss if server crashes mid-game (acceptable for non-critical move logs) + +## Measurements Needed + +Before optimizing, measure current impact: + +- Average JSONB size per move (bytes) +- Average moves per game +- Total storage per game (moves + overhead) +- Query patterns: how often is per-move data actually read? + +## Dependencies + +- None (independent infrastructure improvement) diff --git a/server/.env.example b/server/.env.example index 6cb1716..fdeea33 100644 --- a/server/.env.example +++ b/server/.env.example @@ -7,6 +7,24 @@ PORT=8000 DEBUG=true LOG_LEVEL=DEBUG +# Per-module log level overrides (optional) +# These override LOG_LEVEL for specific modules. +# LOG_LEVEL_GAME=DEBUG # Core game logic +# LOG_LEVEL_AI=DEBUG # AI decisions (very verbose at DEBUG) +# LOG_LEVEL_HANDLERS=DEBUG # WebSocket message handlers +# LOG_LEVEL_ROOM=DEBUG # Room/lobby management +# LOG_LEVEL_AUTH=DEBUG # Auth stack (auth, routers.auth, services.auth_service) +# LOG_LEVEL_STORES=DEBUG # Database/Redis operations + +# --- Preset examples --- +# Staging (debug game logic, quiet everything else): +# LOG_LEVEL=INFO +# LOG_LEVEL_GAME=DEBUG +# LOG_LEVEL_AI=DEBUG +# +# Production (minimal logging): +# LOG_LEVEL=WARNING + # Environment (development, staging, production) # Affects logging format, security headers (HSTS), etc. ENVIRONMENT=development diff --git a/server/logging_config.py b/server/logging_config.py index c8ce052..58a656a 100644 --- a/server/logging_config.py +++ b/server/logging_config.py @@ -148,6 +148,39 @@ class DevelopmentFormatter(logging.Formatter): return output +# Per-module log level overrides via env vars. +# Key: env var suffix, Value: list of Python logger names to apply to. +MODULE_LOGGER_MAP = { + "GAME": ["game"], + "AI": ["ai"], + "HANDLERS": ["handlers"], + "ROOM": ["room"], + "AUTH": ["auth", "routers.auth", "services.auth_service"], + "STORES": ["stores"], +} + + +def _apply_module_overrides() -> dict[str, str]: + """ + Apply per-module log level overrides from LOG_LEVEL_{MODULE} env vars. + + Returns: + Dict of module name -> level for any overrides that were applied. + """ + active = {} + for module, logger_names in MODULE_LOGGER_MAP.items(): + env_val = os.environ.get(f"LOG_LEVEL_{module}", "").upper() + if not env_val: + continue + level = getattr(logging, env_val, None) + if level is None: + continue + active[module] = env_val + for name in logger_names: + logging.getLogger(name).setLevel(level) + return active + + def setup_logging( level: str = "INFO", environment: str = "development", @@ -182,12 +215,19 @@ def setup_logging( logging.getLogger("websockets").setLevel(logging.WARNING) logging.getLogger("asyncio").setLevel(logging.WARNING) + # Apply per-module overrides from env vars + overrides = _apply_module_overrides() + # Log startup logger = logging.getLogger(__name__) logger.info( f"Logging configured: level={level}, environment={environment}", extra={"level": level, "environment": environment}, ) + if overrides: + logger.info( + f"Per-module log level overrides: {', '.join(f'{m}={l}' for m, l in overrides.items())}", + ) class ContextLogger(logging.LoggerAdapter):