Files
AI/ontology_platform/ont_platform/audit/logger.py

263 lines
7.4 KiB
Python
Raw Normal View History

"""감시 로거.
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()