- Multi-stage Dockerfile (builder+runner, <500MB target) - docker-compose.yml: app(x4) + postgres + redis + prometheus + grafana + nginx - .env.example with all required secrets (never hardcoded) - requirements.txt with all dependencies including prometheus-client, alembic - GitHub Actions CI: lint (flake8+bandit+safety) + tests + Docker build/push - GitHub Actions CD: staging deploy -> smoke tests -> production deploy + rollback - Alembic migration setup + initial PostgreSQL schema (001_initial_schema) - SQLite→PostgreSQL data migration script - Prometheus metrics module (HTTP, ML, DB, business metrics) - Prometheus alert rules (5xx >1%, latency >2s, disk >80%, ML accuracy) - Grafana dashboard (overview: req/s, p95, ML accuracy, error rate) - Nginx reverse proxy config (HTTPS/TLS, rate limiting, security headers) - Structured JSON logging module - Automated daily DB backup script (pg_dump + 30-day retention) Branch: feature/devops-cicd Co-Authored-By: Paperclip <noreply@paperclip.ing>
113 lines
3.4 KiB
Python
113 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Structured JSON logging for Turf SaaS.
|
|
Replaces default Flask/Python logging with JSON output suitable for log aggregation.
|
|
"""
|
|
|
|
import logging
|
|
import sys
|
|
import os
|
|
import json
|
|
import traceback
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
|
|
class JSONFormatter(logging.Formatter):
|
|
"""Format log records as JSON lines."""
|
|
|
|
def __init__(self, service_name: str = "turf-saas", env: str = "production"):
|
|
super().__init__()
|
|
self.service_name = service_name
|
|
self.env = env
|
|
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
log_entry = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"level": record.levelname,
|
|
"service": self.service_name,
|
|
"env": self.env,
|
|
"logger": record.name,
|
|
"message": record.getMessage(),
|
|
"module": record.module,
|
|
"function": record.funcName,
|
|
"line": record.lineno,
|
|
}
|
|
|
|
# Add extra fields if present
|
|
if hasattr(record, "request_id"):
|
|
log_entry["request_id"] = record.request_id
|
|
if hasattr(record, "user_id"):
|
|
log_entry["user_id"] = record.user_id
|
|
if hasattr(record, "duration_ms"):
|
|
log_entry["duration_ms"] = record.duration_ms
|
|
if hasattr(record, "status_code"):
|
|
log_entry["status_code"] = record.status_code
|
|
if hasattr(record, "endpoint"):
|
|
log_entry["endpoint"] = record.endpoint
|
|
|
|
# Exception info
|
|
if record.exc_info:
|
|
log_entry["exception"] = {
|
|
"type": record.exc_info[0].__name__ if record.exc_info[0] else None,
|
|
"message": str(record.exc_info[1]) if record.exc_info[1] else None,
|
|
"traceback": traceback.format_exception(*record.exc_info),
|
|
}
|
|
|
|
return json.dumps(log_entry, ensure_ascii=False)
|
|
|
|
|
|
def setup_logging(
|
|
service_name: str = "turf-saas",
|
|
level: Optional[str] = None,
|
|
use_json: bool = True,
|
|
) -> logging.Logger:
|
|
"""
|
|
Configure root logger with JSON or plain formatting.
|
|
|
|
Args:
|
|
service_name: Service name embedded in each log record.
|
|
level: Log level (default: from LOG_LEVEL env var, fallback INFO).
|
|
use_json: Use JSON formatter (True in production, False in dev).
|
|
|
|
Returns:
|
|
Root logger.
|
|
"""
|
|
log_level = level or os.environ.get("LOG_LEVEL", "INFO")
|
|
env = os.environ.get("FLASK_ENV", "production")
|
|
|
|
# Force plain text in dev/testing
|
|
if env in ("development", "testing"):
|
|
use_json = False
|
|
|
|
root_logger = logging.getLogger()
|
|
root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
|
|
|
|
# Remove existing handlers
|
|
root_logger.handlers.clear()
|
|
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
|
|
if use_json:
|
|
handler.setFormatter(JSONFormatter(service_name=service_name, env=env))
|
|
else:
|
|
handler.setFormatter(
|
|
logging.Formatter(
|
|
fmt="%(asctime)s [%(levelname)s] %(name)s — %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
)
|
|
|
|
root_logger.addHandler(handler)
|
|
|
|
# Silence noisy third-party loggers
|
|
for noisy in ["werkzeug", "urllib3", "requests", "gunicorn.access"]:
|
|
logging.getLogger(noisy).setLevel(logging.WARNING)
|
|
|
|
return root_logger
|
|
|
|
|
|
def get_logger(name: str) -> logging.Logger:
|
|
"""Get a named logger."""
|
|
return logging.getLogger(name)
|