"""감시 로거. Phase 8: 감시 로그 기록 및 조회 """ import logging from datetime import datetime, timedelta, UTC from typing import List, Optional, Dict, Any from ont_platform.audit.models import AuditLog, AuditAction, ResourceType, AuditQuery, Change logger = logging.getLogger(__name__) class AuditLogger: """감시 로거.""" def __init__(self, neo4j_adapter=None): """초기화. Args: neo4j_adapter: Neo4j 어댑터 (선택사항) """ self.adapter = neo4j_adapter self.in_memory_logs: List[AuditLog] = [] # 테스트용 메모리 저장소 async def log_action( self, org_id: str, user_id: str, action: AuditAction, resource_type: ResourceType, resource_id: str, changes: Optional[List[Change]] = None, ip_address: Optional[str] = None, user_agent: Optional[str] = None, status: str = "success", error_message: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, ) -> AuditLog: """작업 로그 기록.""" log_entry = AuditLog( org_id=org_id, user_id=user_id, action=action, resource_type=resource_type, resource_id=resource_id, timestamp=datetime.now(UTC), ip_address=ip_address, user_agent=user_agent, status=status, error_message=error_message, changes=changes or [], metadata=metadata or {}, ) # 메모리에 저장 (테스트) self.in_memory_logs.append(log_entry) # Neo4j에 저장 (프로덕션) if self.adapter: try: await self._save_to_neo4j(log_entry) except Exception as e: logger.error(f"Failed to save audit log to Neo4j: {e}") # 로그 출력 logger.info( f"AUDIT: {action.value} {resource_type.value} " f"{resource_id} by {user_id} in {org_id}" ) return log_entry async def _save_to_neo4j(self, log_entry: AuditLog) -> None: """Neo4j에 감시 로그 저장.""" if not self.adapter: return cypher = """ CREATE (log:AuditLog { audit_id: $audit_id, org_id: $org_id, user_id: $user_id, action: $action, resource_type: $resource_type, resource_id: $resource_id, timestamp: $timestamp, ip_address: $ip_address, user_agent: $user_agent, status: $status, error_message: $error_message }) """ params = log_entry.to_neo4j_dict() try: await self.adapter.execute_cypher(cypher, params) except Exception as e: logger.error(f"Failed to save audit log: {e}") raise async def get_audit_trail( self, org_id: str, resource_id: str, limit: int = 100, ) -> List[AuditLog]: """리소스의 변경 이력 조회.""" # 메모리에서 조회 (테스트) logs = [ log for log in self.in_memory_logs if log.org_id == org_id and log.resource_id == resource_id ] logs.sort(key=lambda x: x.timestamp, reverse=True) return logs[:limit] async def query_logs(self, query: AuditQuery) -> tuple[List[AuditLog], int]: """감시 로그 쿼리. Returns: (로그 리스트, 전체 개수) """ # 메모리에서 필터링 (테스트) filtered = self.in_memory_logs.copy() if query.org_id: filtered = [log for log in filtered if log.org_id == query.org_id] if query.user_id: filtered = [log for log in filtered if log.user_id == query.user_id] if query.action: filtered = [log for log in filtered if log.action == query.action] if query.resource_type: filtered = [ log for log in filtered if log.resource_type == query.resource_type ] if query.resource_id: filtered = [log for log in filtered if log.resource_id == query.resource_id] if query.status: filtered = [log for log in filtered if log.status == query.status] if query.start_time: filtered = [ log for log in filtered if log.timestamp >= query.start_time ] if query.end_time: filtered = [log for log in filtered if log.timestamp <= query.end_time] # 정렬 및 페이징 filtered.sort(key=lambda x: x.timestamp, reverse=True) total = len(filtered) paginated = filtered[query.offset : query.offset + query.limit] return paginated, total async def get_user_activities( self, org_id: str, user_id: str, days: int = 7, ) -> List[AuditLog]: """사용자의 최근 활동 조회.""" start_time = datetime.utcnow() - timedelta(days=days) query = AuditQuery( org_id=org_id, user_id=user_id, start_time=start_time, limit=1000, ) logs, _ = await self.query_logs(query) return logs async def get_resource_changes( self, org_id: str, resource_id: str, ) -> List[Dict[str, Any]]: """리소스의 모든 변경사항 조회.""" logs = await self.get_audit_trail(org_id, resource_id, limit=1000) # 변경사항 추출 changes_list = [] for log in logs: if log.changes: changes_list.append( { "timestamp": log.timestamp.isoformat(), "action": log.action.value, "user_id": log.user_id, "changes": [c.to_dict() if isinstance(c, Change) else c for c in log.changes], } ) return changes_list async def get_statistics( self, org_id: str, days: int = 30, ) -> Dict[str, Any]: """감시 통계. Returns: 통계 딕셔너리 """ start_time = datetime.now(UTC) - timedelta(days=days) query = AuditQuery( org_id=org_id, start_time=start_time, limit=10000, ) logs, total = await self.query_logs(query) # 작업별 집계 action_counts = {} for log in logs: action_key = log.action.value action_counts[action_key] = action_counts.get(action_key, 0) + 1 # 사용자별 집계 user_counts = {} for log in logs: user_key = log.user_id user_counts[user_key] = user_counts.get(user_key, 0) + 1 # 상태별 집계 status_counts = { "success": sum(1 for log in logs if log.status == "success"), "failed": sum(1 for log in logs if log.status == "failed"), } return { "period_days": days, "total_logs": total, "success_count": status_counts.get("success", 0), "failed_count": status_counts.get("failed", 0), "by_action": action_counts, "by_user": user_counts, } def clear_in_memory_logs(self) -> None: """메모리 로그 삭제 (테스트용).""" self.in_memory_logs.clear()