Enhance Docker deployment: structured logging, health checks, resource limits, entrypoint script
- Add structured logging config with JSON/text format support - Create docker-entrypoint.sh with dependency health checks - Enhance /health endpoint with DB and Redis connectivity checks - Add resource limits (CPU/memory) for all services - Add network aliases for service discovery - Add container hostnames for better identification - Reduce health check timeout from 20s to 5s - Add build metadata labels to Dockerfile - Use ENTRYPOINT for dependency checking before app startup - Log rotation: 50m per file, 5 files max
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Structured logging configuration for MTG Online backend.
|
||||
|
||||
Supports JSON and text log formats with container metadata.
|
||||
Configured via MTG_LOG_LEVEL and MTG_LOG_FORMAT environment variables.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def _get_container_name() -> str:
|
||||
"""Get container name from HOSTNAME env var or fallback."""
|
||||
return os.environ.get("HOSTNAME", "unknown")
|
||||
|
||||
|
||||
class ContainerFormatter(logging.Formatter):
|
||||
"""Formatter that adds container metadata to log records.
|
||||
|
||||
Supports both text and JSON output formats.
|
||||
"""
|
||||
|
||||
SERVICE_NAME = "mtgonline-backend"
|
||||
|
||||
def __init__(self, fmt_type: str = "text"):
|
||||
super().__init__()
|
||||
self.fmt_type = fmt_type
|
||||
self.container_name = _get_container_name()
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
record.service = self.SERVICE_NAME
|
||||
record.container = self.container_name
|
||||
|
||||
if self.fmt_type == "json":
|
||||
return self._format_json(record)
|
||||
return self._format_text(record)
|
||||
|
||||
def _format_text(self, record: logging.LogRecord) -> str:
|
||||
return (
|
||||
f"{self.formatTime(record)} [{record.service}] "
|
||||
f"{record.levelname} - {record.getMessage()}"
|
||||
)
|
||||
|
||||
def _format_json(self, record: logging.LogRecord) -> str:
|
||||
log_entry = {
|
||||
"timestamp": datetime.fromtimestamp(
|
||||
record.created, tz=timezone.utc
|
||||
).isoformat(),
|
||||
"level": record.levelname,
|
||||
"service": record.service,
|
||||
"container": record.container,
|
||||
"logger": record.name,
|
||||
"message": record.getMessage(),
|
||||
}
|
||||
if record.exc_info and record.exc_info[0] is not None:
|
||||
log_entry["exception"] = self.formatException(record.exc_info)
|
||||
return json.dumps(log_entry, default=str)
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
"""Configure application logging based on environment variables.
|
||||
|
||||
Reads:
|
||||
MTG_LOG_LEVEL - Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
MTG_LOG_FORMAT - Output format ("text" or "json")
|
||||
"""
|
||||
log_level = os.environ.get("MTG_LOG_LEVEL", "INFO").upper()
|
||||
log_format = os.environ.get("MTG_LOG_FORMAT", "text").lower()
|
||||
|
||||
level = getattr(logging, log_level, logging.INFO)
|
||||
|
||||
formatter = ContainerFormatter(fmt_type=log_format)
|
||||
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(level)
|
||||
root_logger.handlers.clear()
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
# Suppress noisy loggers unless DEBUG
|
||||
if level != logging.DEBUG:
|
||||
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
||||
logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(
|
||||
f"Logging configured: level={log_level}, "
|
||||
f"format={log_format}, container={_get_container_name()}"
|
||||
)
|
||||
|
||||
|
||||
# Auto-configure on import
|
||||
configure_logging()
|
||||
+37
-33
@@ -21,32 +21,14 @@ from typing import AsyncGenerator
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.settings import get_settings
|
||||
from app.core.database import engine, mtg_engine, async_session, mtg_async_session
|
||||
from app.routers import auth, users, decks, rooms, admin, card_router, interactions, refresh, user_data, card_import
|
||||
from app.routers.games import router as games_router
|
||||
from app.services.mtgjson_manager import MTGJSONManager
|
||||
|
||||
|
||||
def setup_logging(debug: bool = False) -> None:
|
||||
"""Configure application logging with verbose support."""
|
||||
level = logging.DEBUG if debug else logging.INFO
|
||||
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
]
|
||||
)
|
||||
|
||||
if debug:
|
||||
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
|
||||
logging.getLogger('sqlalchemy.pool').setLevel(logging.WARNING)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Logging initialized at level {logging.getLevelName(level)}")
|
||||
from app.core.logging_config import configure_logging
|
||||
|
||||
|
||||
async def run_initial_download():
|
||||
@@ -91,8 +73,8 @@ def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""Application lifespan events for startup and shutdown."""
|
||||
settings = get_settings()
|
||||
|
||||
# Startup
|
||||
setup_logging(debug=settings.DEBUG)
|
||||
# Configure logging
|
||||
configure_logging()
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"MTG Online Backend starting (v{settings.APP_VERSION})")
|
||||
logger.info(f"Database: {settings.DATABASE_URL.split('@')[1] if '@' in settings.DATABASE_URL else 'configured'}")
|
||||
@@ -146,24 +128,46 @@ app.include_router(card_import.router, prefix="/api/v1/card-import", tags=["Card
|
||||
|
||||
@app.get("/health", tags=["Health"])
|
||||
async def health_check():
|
||||
"""Health check endpoint with MTGJSON data status."""
|
||||
"""Health check endpoint with full service status."""
|
||||
from app.services.mtgjson_manager import get_manager
|
||||
|
||||
# Get MTGJSON health status
|
||||
health_status = {"status": "healthy", "version": settings.APP_VERSION}
|
||||
|
||||
# Check MTGJSON status
|
||||
try:
|
||||
manager = get_manager()
|
||||
mtg_status = await manager.get_health_status()
|
||||
health_status["mtgjson"] = mtg_status
|
||||
if mtg_status.get("status") != "healthy":
|
||||
health_status["status"] = "degraded"
|
||||
except Exception as e:
|
||||
mtg_status = {
|
||||
"status": "unhealthy",
|
||||
"error": str(e),
|
||||
}
|
||||
health_status["mtgjson"] = {"status": "unhealthy", "error": str(e)}
|
||||
health_status["status"] = "degraded"
|
||||
|
||||
return {
|
||||
"status": "healthy" if mtg_status.get("status") == "healthy" else "degraded",
|
||||
"version": settings.APP_VERSION,
|
||||
"mtgjson": mtg_status,
|
||||
}
|
||||
# Check primary database
|
||||
try:
|
||||
from app.core.database import engine
|
||||
async with engine.connect() as conn:
|
||||
await conn.execute(text("SELECT 1"))
|
||||
health_status["database"] = "healthy"
|
||||
except Exception as e:
|
||||
health_status["database"] = {"status": "unhealthy", "error": str(e)}
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
# Check Redis
|
||||
try:
|
||||
from app.core.redis_client import get_redis
|
||||
redis_client = await get_redis()
|
||||
if redis_client:
|
||||
health_status["redis"] = "healthy"
|
||||
else:
|
||||
health_status["redis"] = {"status": "unhealthy", "error": "Connection failed"}
|
||||
health_status["status"] = "unhealthy"
|
||||
except Exception as e:
|
||||
health_status["redis"] = {"status": "unhealthy", "error": str(e)}
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
return health_status
|
||||
|
||||
|
||||
@app.get("/", tags=["Root"])
|
||||
|
||||
Reference in New Issue
Block a user