Add per-module log level overrides for staging/production
Support LOG_LEVEL_{MODULE} env vars (GAME, AI, HANDLERS, ROOM, AUTH,
STORES) to override the global log level for specific modules. Active
overrides are logged at startup. Includes staging/production presets
in .env.example files and a V3.18 stub doc for PostgreSQL storage
efficiency investigation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1b923838e0
commit
e463d929e3
18
.env.example
18
.env.example
@ -20,6 +20,24 @@ DEBUG=false
|
|||||||
# Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
# Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
LOG_LEVEL=INFO
|
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 name (development, staging, production)
|
||||||
ENVIRONMENT=development
|
ENVIRONMENT=development
|
||||||
|
|
||||||
|
|||||||
57
docs/v3/V3_18_POSTGRES_STORAGE_EFFICIENCY.md
Normal file
57
docs/v3/V3_18_POSTGRES_STORAGE_EFFICIENCY.md
Normal file
@ -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)
|
||||||
@ -7,6 +7,24 @@ PORT=8000
|
|||||||
DEBUG=true
|
DEBUG=true
|
||||||
LOG_LEVEL=DEBUG
|
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)
|
# Environment (development, staging, production)
|
||||||
# Affects logging format, security headers (HSTS), etc.
|
# Affects logging format, security headers (HSTS), etc.
|
||||||
ENVIRONMENT=development
|
ENVIRONMENT=development
|
||||||
|
|||||||
@ -148,6 +148,39 @@ class DevelopmentFormatter(logging.Formatter):
|
|||||||
return output
|
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(
|
def setup_logging(
|
||||||
level: str = "INFO",
|
level: str = "INFO",
|
||||||
environment: str = "development",
|
environment: str = "development",
|
||||||
@ -182,12 +215,19 @@ def setup_logging(
|
|||||||
logging.getLogger("websockets").setLevel(logging.WARNING)
|
logging.getLogger("websockets").setLevel(logging.WARNING)
|
||||||
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
# Apply per-module overrides from env vars
|
||||||
|
overrides = _apply_module_overrides()
|
||||||
|
|
||||||
# Log startup
|
# Log startup
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Logging configured: level={level}, environment={environment}",
|
f"Logging configured: level={level}, environment={environment}",
|
||||||
extra={"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):
|
class ContextLogger(logging.LoggerAdapter):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user