""" JSON-Log für Vereins-Feature-Zugriffe (Phase 2: nur Monitoring, kein Block). Spez: CLUB_MEMBERSHIP_AND_FEATURES.v1.md §9 Phase 2 — analog Mitai feature_logger.py. """ from __future__ import annotations import json import logging import os from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict, Optional def _log_dir() -> Path: custom = (os.getenv("CLUB_FEATURE_LOG_DIR") or "").strip() if custom: return Path(custom) return Path("/app/logs") feature_usage_logger = logging.getLogger("shinkan.club_feature_usage") feature_usage_logger.setLevel(logging.INFO) feature_usage_logger.propagate = False if not feature_usage_logger.handlers: log_dir = _log_dir() try: log_dir.mkdir(parents=True, exist_ok=True) log_file = log_dir / "club-feature-usage.log" file_handler = logging.FileHandler(log_file, encoding="utf-8") file_handler.setLevel(logging.INFO) file_handler.setFormatter(logging.Formatter("%(message)s")) feature_usage_logger.addHandler(file_handler) except OSError: # Dev ohne /app/logs: Fallback stderr stream_handler = logging.StreamHandler() stream_handler.setFormatter(logging.Formatter("[club-feature-usage] %(message)s")) feature_usage_logger.addHandler(stream_handler) def log_club_feature_usage( *, club_id: Optional[int], profile_id: Optional[int], feature_id: str, action: str, access: Dict[str, Any], endpoint: Optional[str] = None, phase: str = "probe", ) -> None: """ Strukturiertes JSON-Log eines Feature-Checks. phase: probe (Phase 2, non-blocking) | enforce (Phase 4, nach Block-Entscheid) """ entry = { "timestamp": datetime.now(timezone.utc).isoformat(), "club_id": club_id, "profile_id": profile_id, "feature": feature_id, "action": action, "endpoint": endpoint, "phase": phase, "plan_id": access.get("plan_id"), "used": access.get("used", 0), "limit": access.get("limit"), "remaining": access.get("remaining"), "allowed": access.get("allowed", True), "reason": access.get("reason", "unknown"), "enforcement": os.getenv("CLUB_FEATURE_ENFORCE", "0") == "1", } feature_usage_logger.info(json.dumps(entry, ensure_ascii=False))