#!/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)