Upload folder using huggingface_hub
Browse files- __init__.py +38 -87
- __pycache__/__init__.cpython-313.pyc +0 -0
- __pycache__/audit.cpython-313.pyc +0 -0
- __pycache__/audit_hash_chain.cpython-313.pyc +0 -0
- __pycache__/logger.cpython-313.pyc +0 -0
- __pycache__/schema.cpython-313.pyc +0 -0
- __pycache__/system_metrics.cpython-313.pyc +0 -0
- audit.py +450 -0
- audit_hash_chain.py +202 -0
- logger.py +225 -0
- schema.py +324 -0
- system_metrics.py +262 -0
__init__.py
CHANGED
|
@@ -1,96 +1,47 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
Provides:
|
| 7 |
-
- Streaming evaluation pipeline for real-time monitoring
|
| 8 |
-
- Drift detection with statistical analysis
|
| 9 |
-
- Alert generation and management
|
| 10 |
-
- Dashboard data API
|
| 11 |
-
|
| 12 |
-
Architecture:
|
| 13 |
-
Live Prompt Stream
|
| 14 |
-
↓
|
| 15 |
-
Streaming Evaluator
|
| 16 |
-
↓
|
| 17 |
-
Defender + Judge
|
| 18 |
-
↓
|
| 19 |
-
Rolling Metrics Store
|
| 20 |
-
↓
|
| 21 |
-
Drift Detection
|
| 22 |
-
↓
|
| 23 |
-
Alerting Engine
|
| 24 |
-
↓
|
| 25 |
-
Dashboard (Monitoring Tab)
|
| 26 |
"""
|
| 27 |
|
| 28 |
-
from .
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
AlertType,
|
| 33 |
-
get_alert_manager,
|
| 34 |
-
)
|
| 35 |
-
from .drift_detection import (
|
| 36 |
-
DriftDetector,
|
| 37 |
-
DriftDetectionResult,
|
| 38 |
-
MetricWindow,
|
| 39 |
-
get_drift_detector,
|
| 40 |
)
|
| 41 |
-
from .
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
MonitoringRequest,
|
| 56 |
-
MonitoringResponse,
|
| 57 |
-
RollingMetrics,
|
| 58 |
-
)
|
| 59 |
-
from .streaming_evaluator import (
|
| 60 |
-
StreamingEvaluator,
|
| 61 |
-
get_streaming_evaluator,
|
| 62 |
)
|
| 63 |
|
| 64 |
__all__ = [
|
| 65 |
-
#
|
| 66 |
-
"
|
| 67 |
-
"
|
| 68 |
-
"
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
"
|
| 73 |
-
"
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
"
|
| 77 |
-
"
|
| 78 |
-
"
|
| 79 |
-
"
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
"
|
| 83 |
-
"Alert",
|
| 84 |
-
"AlertType",
|
| 85 |
-
"AlertSeverity",
|
| 86 |
-
"AlertSummary",
|
| 87 |
-
"get_alert_manager",
|
| 88 |
-
|
| 89 |
-
# Schemas
|
| 90 |
-
"MonitoringRequest",
|
| 91 |
-
"MonitoringResponse",
|
| 92 |
-
"RollingMetrics",
|
| 93 |
]
|
| 94 |
-
|
| 95 |
-
# Version
|
| 96 |
-
__version__ = "0.1.0"
|
|
|
|
| 1 |
"""
|
| 2 |
+
Logging Module
|
| 3 |
|
| 4 |
+
Structured logging framework for AegisLM.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
+
from backend.logging.logger import (
|
| 8 |
+
StructuredLogger,
|
| 9 |
+
get_logger,
|
| 10 |
+
default_logger,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
)
|
| 12 |
+
from backend.logging.schema import (
|
| 13 |
+
LogLevel,
|
| 14 |
+
Component,
|
| 15 |
+
BaseLogSchema,
|
| 16 |
+
EvaluationLogSchema,
|
| 17 |
+
MetricsLogSchema,
|
| 18 |
+
ErrorLogSchema,
|
| 19 |
+
LifecycleLogSchema,
|
| 20 |
+
OrchestratorLogSchema,
|
| 21 |
+
ModelExecutorLogSchema,
|
| 22 |
+
ScoringLogSchema,
|
| 23 |
+
APILogSchema,
|
| 24 |
+
AggregatedMetricsSchema,
|
| 25 |
+
RunSummarySchema,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
)
|
| 27 |
|
| 28 |
__all__ = [
|
| 29 |
+
# Logger
|
| 30 |
+
"StructuredLogger",
|
| 31 |
+
"get_logger",
|
| 32 |
+
"default_logger",
|
| 33 |
+
# Schema
|
| 34 |
+
"LogLevel",
|
| 35 |
+
"Component",
|
| 36 |
+
"BaseLogSchema",
|
| 37 |
+
"EvaluationLogSchema",
|
| 38 |
+
"MetricsLogSchema",
|
| 39 |
+
"ErrorLogSchema",
|
| 40 |
+
"LifecycleLogSchema",
|
| 41 |
+
"OrchestratorLogSchema",
|
| 42 |
+
"ModelExecutorLogSchema",
|
| 43 |
+
"ScoringLogSchema",
|
| 44 |
+
"APILogSchema",
|
| 45 |
+
"AggregatedMetricsSchema",
|
| 46 |
+
"RunSummarySchema",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
]
|
|
|
|
|
|
|
|
|
__pycache__/__init__.cpython-313.pyc
CHANGED
|
Binary files a/__pycache__/__init__.cpython-313.pyc and b/__pycache__/__init__.cpython-313.pyc differ
|
|
|
__pycache__/audit.cpython-313.pyc
ADDED
|
Binary file (15.6 kB). View file
|
|
|
__pycache__/audit_hash_chain.cpython-313.pyc
ADDED
|
Binary file (7.25 kB). View file
|
|
|
__pycache__/logger.cpython-313.pyc
ADDED
|
Binary file (9.52 kB). View file
|
|
|
__pycache__/schema.cpython-313.pyc
ADDED
|
Binary file (10.2 kB). View file
|
|
|
__pycache__/system_metrics.cpython-313.pyc
ADDED
|
Binary file (9.6 kB). View file
|
|
|
audit.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Audit Logging Service
|
| 3 |
+
|
| 4 |
+
Provides audit logging functionality for security and compliance.
|
| 5 |
+
Immutable entries - append only, no updates allowed.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import uuid
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from typing import Any, Dict, Optional
|
| 11 |
+
from enum import Enum
|
| 12 |
+
|
| 13 |
+
from sqlalchemy import select
|
| 14 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 15 |
+
|
| 16 |
+
from backend.db.models import AuditLog, User, Tenant
|
| 17 |
+
from backend.db.session import get_db_session
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class AuditAction(str, Enum):
|
| 21 |
+
"""Standard audit action types."""
|
| 22 |
+
# Job actions
|
| 23 |
+
JOB_CREATED = "JOB_CREATED"
|
| 24 |
+
JOB_STARTED = "JOB_STARTED"
|
| 25 |
+
JOB_COMPLETED = "JOB_COMPLETED"
|
| 26 |
+
JOB_FAILED = "JOB_FAILED"
|
| 27 |
+
JOB_CANCELLED = "JOB_CANCELLED"
|
| 28 |
+
JOB_DELETED = "JOB_DELETED"
|
| 29 |
+
|
| 30 |
+
# User actions
|
| 31 |
+
USER_CREATED = "USER_CREATED"
|
| 32 |
+
USER_UPDATED = "USER_UPDATED"
|
| 33 |
+
USER_DELETED = "USER_DELETED"
|
| 34 |
+
USER_LOGIN = "USER_LOGIN"
|
| 35 |
+
USER_LOGOUT = "USER_LOGOUT"
|
| 36 |
+
ROLE_CHANGED = "ROLE_CHANGED"
|
| 37 |
+
|
| 38 |
+
# API Key actions
|
| 39 |
+
API_KEY_CREATED = "API_KEY_CREATED"
|
| 40 |
+
API_KEY_DELETED = "API_KEY_DELETED"
|
| 41 |
+
API_KEY_ROTATED = "API_KEY_ROTATED"
|
| 42 |
+
|
| 43 |
+
# Release actions
|
| 44 |
+
RELEASE_APPROVED = "RELEASE_APPROVED"
|
| 45 |
+
RELEASE_REJECTED = "RELEASE_REJECTED"
|
| 46 |
+
RELEASE_BLOCKED = "RELEASE_BLOCKED"
|
| 47 |
+
|
| 48 |
+
# Report actions
|
| 49 |
+
REPORT_CREATED = "REPORT_CREATED"
|
| 50 |
+
REPORT_EXPORTED = "REPORT_EXPORTED"
|
| 51 |
+
REPORT_DELETED = "REPORT_DELETED"
|
| 52 |
+
|
| 53 |
+
# Monitoring actions
|
| 54 |
+
ALERT_CREATED = "ALERT_CREATED"
|
| 55 |
+
ALERT_RESOLVED = "ALERT_RESOLVED"
|
| 56 |
+
BASELINE_UPDATED = "BASELINE_UPDATED"
|
| 57 |
+
|
| 58 |
+
# Tenant actions
|
| 59 |
+
TENANT_CREATED = "TENANT_CREATED"
|
| 60 |
+
TENANT_UPDATED = "TENANT_UPDATED"
|
| 61 |
+
TENANT_DEACTIVATED = "TENANT_DEACTIVATED"
|
| 62 |
+
|
| 63 |
+
# Model/Dataset actions
|
| 64 |
+
MODEL_REGISTERED = "MODEL_REGISTERED"
|
| 65 |
+
DATASET_REGISTERED = "DATASET_REGISTERED"
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class ResourceType(str, Enum):
|
| 69 |
+
"""Resource types for audit logging."""
|
| 70 |
+
JOB = "job"
|
| 71 |
+
USER = "user"
|
| 72 |
+
API_KEY = "api_key"
|
| 73 |
+
RELEASE = "release"
|
| 74 |
+
REPORT = "report"
|
| 75 |
+
ALERT = "alert"
|
| 76 |
+
TENANT = "tenant"
|
| 77 |
+
MODEL = "model"
|
| 78 |
+
DATASET = "dataset"
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class AuditLogger:
|
| 82 |
+
"""
|
| 83 |
+
Audit logging service for tracking user actions.
|
| 84 |
+
|
| 85 |
+
All entries are immutable - append only, no updates allowed.
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
def __init__(self, db_session: AsyncSession):
|
| 89 |
+
self.db = db_session
|
| 90 |
+
|
| 91 |
+
async def log(
|
| 92 |
+
self,
|
| 93 |
+
tenant_id: uuid.UUID,
|
| 94 |
+
user_id: uuid.UUID,
|
| 95 |
+
action: AuditAction,
|
| 96 |
+
resource_type: ResourceType,
|
| 97 |
+
resource_id: Optional[uuid.UUID] = None,
|
| 98 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 99 |
+
request_id: Optional[str] = None,
|
| 100 |
+
ip_address: Optional[str] = None,
|
| 101 |
+
status: str = "success",
|
| 102 |
+
error_message: Optional[str] = None,
|
| 103 |
+
) -> AuditLog:
|
| 104 |
+
"""
|
| 105 |
+
Create an audit log entry.
|
| 106 |
+
|
| 107 |
+
Args:
|
| 108 |
+
tenant_id: Tenant ID
|
| 109 |
+
user_id: User who performed the action
|
| 110 |
+
action: Action performed
|
| 111 |
+
resource_type: Type of resource affected
|
| 112 |
+
resource_id: ID of the affected resource
|
| 113 |
+
metadata: Additional action metadata
|
| 114 |
+
request_id: Request ID for tracing
|
| 115 |
+
ip_address: Client IP address
|
| 116 |
+
status: Action status (success, failure, pending)
|
| 117 |
+
error_message: Error message if action failed
|
| 118 |
+
|
| 119 |
+
Returns:
|
| 120 |
+
Created AuditLog entry
|
| 121 |
+
"""
|
| 122 |
+
audit_log = AuditLog(
|
| 123 |
+
tenant_id=tenant_id,
|
| 124 |
+
user_id=user_id,
|
| 125 |
+
action=action.value,
|
| 126 |
+
resource_type=resource_type.value,
|
| 127 |
+
resource_id=resource_id,
|
| 128 |
+
metadata=metadata,
|
| 129 |
+
request_id=request_id,
|
| 130 |
+
ip_address=ip_address,
|
| 131 |
+
status=status,
|
| 132 |
+
error_message=error_message,
|
| 133 |
+
timestamp=datetime.utcnow(),
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
self.db.add(audit_log)
|
| 137 |
+
await self.db.commit()
|
| 138 |
+
await self.db.refresh(audit_log)
|
| 139 |
+
|
| 140 |
+
return audit_log
|
| 141 |
+
|
| 142 |
+
async def log_job_created(
|
| 143 |
+
self,
|
| 144 |
+
tenant_id: uuid.UUID,
|
| 145 |
+
user_id: uuid.UUID,
|
| 146 |
+
job_id: uuid.UUID,
|
| 147 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 148 |
+
**kwargs,
|
| 149 |
+
) -> AuditLog:
|
| 150 |
+
"""Log job creation."""
|
| 151 |
+
return await self.log(
|
| 152 |
+
tenant_id=tenant_id,
|
| 153 |
+
user_id=user_id,
|
| 154 |
+
action=AuditAction.JOB_CREATED,
|
| 155 |
+
resource_type=ResourceType.JOB,
|
| 156 |
+
resource_id=job_id,
|
| 157 |
+
metadata=metadata,
|
| 158 |
+
**kwargs,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
async def log_job_completed(
|
| 162 |
+
self,
|
| 163 |
+
tenant_id: uuid.UUID,
|
| 164 |
+
user_id: uuid.UUID,
|
| 165 |
+
job_id: uuid.UUID,
|
| 166 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 167 |
+
**kwargs,
|
| 168 |
+
) -> AuditLog:
|
| 169 |
+
"""Log job completion."""
|
| 170 |
+
return await self.log(
|
| 171 |
+
tenant_id=tenant_id,
|
| 172 |
+
user_id=user_id,
|
| 173 |
+
action=AuditAction.JOB_COMPLETED,
|
| 174 |
+
resource_type=ResourceType.JOB,
|
| 175 |
+
resource_id=job_id,
|
| 176 |
+
metadata=metadata,
|
| 177 |
+
**kwargs,
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
async def log_job_failed(
|
| 181 |
+
self,
|
| 182 |
+
tenant_id: uuid.UUID,
|
| 183 |
+
user_id: uuid.UUID,
|
| 184 |
+
job_id: uuid.UUID,
|
| 185 |
+
error_message: str,
|
| 186 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 187 |
+
**kwargs,
|
| 188 |
+
) -> AuditLog:
|
| 189 |
+
"""Log job failure."""
|
| 190 |
+
return await self.log(
|
| 191 |
+
tenant_id=tenant_id,
|
| 192 |
+
user_id=user_id,
|
| 193 |
+
action=AuditAction.JOB_FAILED,
|
| 194 |
+
resource_type=ResourceType.JOB,
|
| 195 |
+
resource_id=job_id,
|
| 196 |
+
metadata=metadata,
|
| 197 |
+
status="failure",
|
| 198 |
+
error_message=error_message,
|
| 199 |
+
**kwargs,
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
async def log_api_key_created(
|
| 203 |
+
self,
|
| 204 |
+
tenant_id: uuid.UUID,
|
| 205 |
+
user_id: uuid.UUID,
|
| 206 |
+
api_key_id: uuid.UUID,
|
| 207 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 208 |
+
**kwargs,
|
| 209 |
+
) -> AuditLog:
|
| 210 |
+
"""Log API key creation."""
|
| 211 |
+
return await self.log(
|
| 212 |
+
tenant_id=tenant_id,
|
| 213 |
+
user_id=user_id,
|
| 214 |
+
action=AuditAction.API_KEY_CREATED,
|
| 215 |
+
resource_type=ResourceType.API_KEY,
|
| 216 |
+
resource_id=api_key_id,
|
| 217 |
+
metadata=metadata,
|
| 218 |
+
**kwargs,
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
async def log_release_approved(
|
| 222 |
+
self,
|
| 223 |
+
tenant_id: uuid.UUID,
|
| 224 |
+
user_id: uuid.UUID,
|
| 225 |
+
release_id: uuid.UUID,
|
| 226 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 227 |
+
**kwargs,
|
| 228 |
+
) -> AuditLog:
|
| 229 |
+
"""Log release approval."""
|
| 230 |
+
return await self.log(
|
| 231 |
+
tenant_id=tenant_id,
|
| 232 |
+
user_id=user_id,
|
| 233 |
+
action=AuditAction.RELEASE_APPROVED,
|
| 234 |
+
resource_type=ResourceType.RELEASE,
|
| 235 |
+
resource_id=release_id,
|
| 236 |
+
metadata=metadata,
|
| 237 |
+
**kwargs,
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
async def log_user_login(
|
| 241 |
+
self,
|
| 242 |
+
tenant_id: uuid.UUID,
|
| 243 |
+
user_id: uuid.UUID,
|
| 244 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 245 |
+
**kwargs,
|
| 246 |
+
) -> AuditLog:
|
| 247 |
+
"""Log user login."""
|
| 248 |
+
return await self.log(
|
| 249 |
+
tenant_id=tenant_id,
|
| 250 |
+
user_id=user_id,
|
| 251 |
+
action=AuditAction.USER_LOGIN,
|
| 252 |
+
resource_type=ResourceType.USER,
|
| 253 |
+
resource_id=user_id,
|
| 254 |
+
metadata=metadata,
|
| 255 |
+
**kwargs,
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
async def log_role_changed(
|
| 259 |
+
self,
|
| 260 |
+
tenant_id: uuid.UUID,
|
| 261 |
+
user_id: uuid.UUID,
|
| 262 |
+
target_user_id: uuid.UUID,
|
| 263 |
+
old_role: str,
|
| 264 |
+
new_role: str,
|
| 265 |
+
**kwargs,
|
| 266 |
+
) -> AuditLog:
|
| 267 |
+
"""Log role change."""
|
| 268 |
+
return await self.log(
|
| 269 |
+
tenant_id=tenant_id,
|
| 270 |
+
user_id=user_id,
|
| 271 |
+
action=AuditAction.ROLE_CHANGED,
|
| 272 |
+
resource_type=ResourceType.USER,
|
| 273 |
+
resource_id=target_user_id,
|
| 274 |
+
metadata={"old_role": old_role, "new_role": new_role},
|
| 275 |
+
**kwargs,
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
async def log_report_exported(
|
| 279 |
+
self,
|
| 280 |
+
tenant_id: uuid.UUID,
|
| 281 |
+
user_id: uuid.UUID,
|
| 282 |
+
report_id: uuid.UUID,
|
| 283 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 284 |
+
**kwargs,
|
| 285 |
+
) -> AuditLog:
|
| 286 |
+
"""Log report export."""
|
| 287 |
+
return await self.log(
|
| 288 |
+
tenant_id=tenant_id,
|
| 289 |
+
user_id=user_id,
|
| 290 |
+
action=AuditAction.REPORT_EXPORTED,
|
| 291 |
+
resource_type=ResourceType.REPORT,
|
| 292 |
+
resource_id=report_id,
|
| 293 |
+
metadata=metadata,
|
| 294 |
+
**kwargs,
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
async def get_tenant_audit_logs(
|
| 298 |
+
self,
|
| 299 |
+
tenant_id: uuid.UUID,
|
| 300 |
+
limit: int = 100,
|
| 301 |
+
offset: int = 0,
|
| 302 |
+
action: Optional[AuditAction] = None,
|
| 303 |
+
user_id: Optional[uuid.UUID] = None,
|
| 304 |
+
resource_type: Optional[ResourceType] = None,
|
| 305 |
+
) -> list[AuditLog]:
|
| 306 |
+
"""
|
| 307 |
+
Get audit logs for a tenant.
|
| 308 |
+
|
| 309 |
+
Args:
|
| 310 |
+
tenant_id: Tenant ID
|
| 311 |
+
limit: Maximum number of entries to return
|
| 312 |
+
offset: Number of entries to skip
|
| 313 |
+
action: Filter by action type
|
| 314 |
+
user_id: Filter by user
|
| 315 |
+
resource_type: Filter by resource type
|
| 316 |
+
|
| 317 |
+
Returns:
|
| 318 |
+
List of audit log entries
|
| 319 |
+
"""
|
| 320 |
+
query = select(AuditLog).where(
|
| 321 |
+
AuditLog.tenant_id == tenant_id
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
if action:
|
| 325 |
+
query = query.where(AuditLog.action == action.value)
|
| 326 |
+
|
| 327 |
+
if user_id:
|
| 328 |
+
query = query.where(AuditLog.user_id == user_id)
|
| 329 |
+
|
| 330 |
+
if resource_type:
|
| 331 |
+
query = query.where(AuditLog.resource_type == resource_type.value)
|
| 332 |
+
|
| 333 |
+
query = (
|
| 334 |
+
query
|
| 335 |
+
.order_by(AuditLog.timestamp.desc())
|
| 336 |
+
.limit(limit)
|
| 337 |
+
.offset(offset)
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
result = await self.db.execute(query)
|
| 341 |
+
return list(result.scalars().all())
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
async def create_audit_log(
|
| 345 |
+
tenant_id: uuid.UUID,
|
| 346 |
+
user_id: uuid.UUID,
|
| 347 |
+
action: AuditAction,
|
| 348 |
+
resource_type: ResourceType,
|
| 349 |
+
resource_id: Optional[uuid.UUID] = None,
|
| 350 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 351 |
+
request_id: Optional[str] = None,
|
| 352 |
+
ip_address: Optional[str] = None,
|
| 353 |
+
status: str = "success",
|
| 354 |
+
error_message: Optional[str] = None,
|
| 355 |
+
) -> AuditLog:
|
| 356 |
+
"""
|
| 357 |
+
Convenience function to create an audit log entry.
|
| 358 |
+
|
| 359 |
+
Creates a new database session for the operation.
|
| 360 |
+
"""
|
| 361 |
+
async with get_db_session() as session:
|
| 362 |
+
logger = AuditLogger(session)
|
| 363 |
+
return await logger.log(
|
| 364 |
+
tenant_id=tenant_id,
|
| 365 |
+
user_id=user_id,
|
| 366 |
+
action=action,
|
| 367 |
+
resource_type=resource_type,
|
| 368 |
+
resource_id=resource_id,
|
| 369 |
+
metadata=metadata,
|
| 370 |
+
request_id=request_id,
|
| 371 |
+
ip_address=ip_address,
|
| 372 |
+
status=status,
|
| 373 |
+
error_message=error_message,
|
| 374 |
+
)
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
# =============================================================================
|
| 378 |
+
# Audit context manager for automatic logging
|
| 379 |
+
# =============================================================================
|
| 380 |
+
|
| 381 |
+
class AuditContext:
|
| 382 |
+
"""
|
| 383 |
+
Context manager for automatic audit logging.
|
| 384 |
+
|
| 385 |
+
Usage:
|
| 386 |
+
async with AuditContext(
|
| 387 |
+
tenant_id=tenant_id,
|
| 388 |
+
user_id=user_id,
|
| 389 |
+
action=AuditAction.JOB_CREATED,
|
| 390 |
+
resource_type=ResourceType.JOB,
|
| 391 |
+
resource_id=job_id,
|
| 392 |
+
) as audit:
|
| 393 |
+
# Do work
|
| 394 |
+
await process_job(job_id)
|
| 395 |
+
# Success automatically logged on exit
|
| 396 |
+
"""
|
| 397 |
+
|
| 398 |
+
def __init__(
|
| 399 |
+
self,
|
| 400 |
+
tenant_id: uuid.UUID,
|
| 401 |
+
user_id: uuid.UUID,
|
| 402 |
+
action: AuditAction,
|
| 403 |
+
resource_type: ResourceType,
|
| 404 |
+
resource_id: Optional[uuid.UUID] = None,
|
| 405 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 406 |
+
request_id: Optional[str] = None,
|
| 407 |
+
ip_address: Optional[str] = None,
|
| 408 |
+
):
|
| 409 |
+
self.tenant_id = tenant_id
|
| 410 |
+
self.user_id = user_id
|
| 411 |
+
self.action = action
|
| 412 |
+
self.resource_type = resource_type
|
| 413 |
+
self.resource_id = resource_id
|
| 414 |
+
self.metadata = metadata or {}
|
| 415 |
+
self.request_id = request_id
|
| 416 |
+
self.ip_address = ip_address
|
| 417 |
+
self.status = "success"
|
| 418 |
+
self.error_message = None
|
| 419 |
+
self._audit_log: Optional[AuditLog] = None
|
| 420 |
+
|
| 421 |
+
async def __aenter__(self) -> "AuditContext":
|
| 422 |
+
return self
|
| 423 |
+
|
| 424 |
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
| 425 |
+
if exc_type is not None:
|
| 426 |
+
self.status = "failure"
|
| 427 |
+
self.error_message = str(exc_val) if exc_val else "Unknown error"
|
| 428 |
+
|
| 429 |
+
# Log the action
|
| 430 |
+
async with get_db_session() as session:
|
| 431 |
+
logger = AuditLogger(session)
|
| 432 |
+
self._audit_log = await logger.log(
|
| 433 |
+
tenant_id=self.tenant_id,
|
| 434 |
+
user_id=self.user_id,
|
| 435 |
+
action=self.action,
|
| 436 |
+
resource_type=self.resource_type,
|
| 437 |
+
resource_id=self.resource_id,
|
| 438 |
+
metadata=self.metadata,
|
| 439 |
+
request_id=self.request_id,
|
| 440 |
+
ip_address=self.ip_address,
|
| 441 |
+
status=self.status,
|
| 442 |
+
error_message=self.error_message,
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
return False # Don't suppress exceptions
|
| 446 |
+
|
| 447 |
+
@property
|
| 448 |
+
def audit_log(self) -> Optional[AuditLog]:
|
| 449 |
+
"""Get the created audit log entry."""
|
| 450 |
+
return self._audit_log
|
audit_hash_chain.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Audit Hash Chain for AegisLM
|
| 3 |
+
|
| 4 |
+
Provides hash chain functionality for tamper-evident audit logging.
|
| 5 |
+
Each audit entry contains a hash that includes the previous entry's hash,
|
| 6 |
+
creating a chain that can be verified for integrity.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import hashlib
|
| 10 |
+
import uuid
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from typing import Any, Dict, List, Tuple
|
| 13 |
+
|
| 14 |
+
from sqlalchemy import select
|
| 15 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 16 |
+
|
| 17 |
+
from backend.db.models import AuditLog
|
| 18 |
+
from backend.db.session import get_db_session
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class AuditHashChain:
|
| 22 |
+
"""
|
| 23 |
+
Manages the audit log hash chain for tamper detection.
|
| 24 |
+
|
| 25 |
+
Each audit entry contains a hash that includes the previous entry's hash,
|
| 26 |
+
creating a chain that can be verified for integrity.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
GENESIS_HASH = "0" * 64 # 64-character hex string (SHA256)
|
| 30 |
+
|
| 31 |
+
@staticmethod
|
| 32 |
+
def compute_entry_hash(
|
| 33 |
+
previous_hash: str,
|
| 34 |
+
tenant_id: uuid.UUID,
|
| 35 |
+
user_id: uuid.UUID,
|
| 36 |
+
action: str,
|
| 37 |
+
resource_type: str,
|
| 38 |
+
resource_id: uuid.UUID | None,
|
| 39 |
+
timestamp: datetime,
|
| 40 |
+
status: str,
|
| 41 |
+
) -> str:
|
| 42 |
+
"""
|
| 43 |
+
Compute SHA256 hash for an audit entry.
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
previous_hash: Hash of the previous entry
|
| 47 |
+
tenant_id: Tenant ID
|
| 48 |
+
user_id: User who performed the action
|
| 49 |
+
action: Action performed
|
| 50 |
+
resource_type: Type of resource
|
| 51 |
+
resource_id: ID of the resource
|
| 52 |
+
timestamp: When the action occurred
|
| 53 |
+
status: Action status
|
| 54 |
+
|
| 55 |
+
Returns:
|
| 56 |
+
SHA256 hash as hex string
|
| 57 |
+
"""
|
| 58 |
+
# Create deterministic string representation
|
| 59 |
+
entry_data = "|".join([
|
| 60 |
+
previous_hash,
|
| 61 |
+
str(tenant_id),
|
| 62 |
+
str(user_id),
|
| 63 |
+
action,
|
| 64 |
+
resource_type,
|
| 65 |
+
str(resource_id) if resource_id else "",
|
| 66 |
+
timestamp.isoformat(),
|
| 67 |
+
status,
|
| 68 |
+
])
|
| 69 |
+
|
| 70 |
+
return hashlib.sha256(entry_data.encode()).hexdigest()
|
| 71 |
+
|
| 72 |
+
@staticmethod
|
| 73 |
+
def compute_hash_from_log(log: AuditLog, previous_hash: str) -> str:
|
| 74 |
+
"""
|
| 75 |
+
Compute hash from an AuditLog object.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
log: AuditLog object
|
| 79 |
+
previous_hash: Hash of previous entry
|
| 80 |
+
|
| 81 |
+
Returns:
|
| 82 |
+
SHA256 hash as hex string
|
| 83 |
+
"""
|
| 84 |
+
return AuditHashChain.compute_entry_hash(
|
| 85 |
+
previous_hash=previous_hash,
|
| 86 |
+
tenant_id=log.tenant_id,
|
| 87 |
+
user_id=log.user_id,
|
| 88 |
+
action=log.action,
|
| 89 |
+
resource_type=log.resource_type,
|
| 90 |
+
resource_id=log.resource_id,
|
| 91 |
+
timestamp=log.timestamp,
|
| 92 |
+
status=log.status,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
@classmethod
|
| 96 |
+
async def verify_chain(
|
| 97 |
+
cls,
|
| 98 |
+
db: AsyncSession,
|
| 99 |
+
tenant_id: uuid.UUID,
|
| 100 |
+
) -> Tuple[bool, List[Dict[str, Any]]]:
|
| 101 |
+
"""
|
| 102 |
+
Verify the integrity of the audit log hash chain for a tenant.
|
| 103 |
+
|
| 104 |
+
Args:
|
| 105 |
+
db: Database session
|
| 106 |
+
tenant_id: Tenant ID
|
| 107 |
+
|
| 108 |
+
Returns:
|
| 109 |
+
Tuple of (is_valid, list of issues)
|
| 110 |
+
"""
|
| 111 |
+
# Get all audit logs for tenant, ordered by timestamp
|
| 112 |
+
query = (
|
| 113 |
+
select(AuditLog)
|
| 114 |
+
.where(AuditLog.tenant_id == tenant_id)
|
| 115 |
+
.order_by(AuditLog.timestamp.asc())
|
| 116 |
+
)
|
| 117 |
+
result = await db.execute(query)
|
| 118 |
+
logs = list(result.scalars().all())
|
| 119 |
+
|
| 120 |
+
issues = []
|
| 121 |
+
previous_hash = cls.GENESIS_HASH
|
| 122 |
+
|
| 123 |
+
for i, log in enumerate(logs):
|
| 124 |
+
# Check if stored hash matches computed hash
|
| 125 |
+
stored_hash = getattr(log, 'entry_hash', None)
|
| 126 |
+
if stored_hash is None:
|
| 127 |
+
issues.append({
|
| 128 |
+
"index": i,
|
| 129 |
+
"log_id": str(log.id),
|
| 130 |
+
"issue": "No hash stored",
|
| 131 |
+
})
|
| 132 |
+
continue
|
| 133 |
+
|
| 134 |
+
computed_hash = cls.compute_hash_from_log(log, previous_hash)
|
| 135 |
+
|
| 136 |
+
if stored_hash != computed_hash:
|
| 137 |
+
issues.append({
|
| 138 |
+
"index": i,
|
| 139 |
+
"log_id": str(log.id),
|
| 140 |
+
"issue": "Hash mismatch",
|
| 141 |
+
"expected": computed_hash,
|
| 142 |
+
"actual": stored_hash,
|
| 143 |
+
})
|
| 144 |
+
|
| 145 |
+
# Move to next
|
| 146 |
+
previous_hash = stored_hash
|
| 147 |
+
|
| 148 |
+
return len(issues) == 0, issues
|
| 149 |
+
|
| 150 |
+
@classmethod
|
| 151 |
+
async def get_chain_status(
|
| 152 |
+
cls,
|
| 153 |
+
db: AsyncSession,
|
| 154 |
+
tenant_id: uuid.UUID,
|
| 155 |
+
) -> Dict[str, Any]:
|
| 156 |
+
"""
|
| 157 |
+
Get the status of the audit hash chain.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
db: Database session
|
| 161 |
+
tenant_id: Tenant ID
|
| 162 |
+
|
| 163 |
+
Returns:
|
| 164 |
+
Dictionary with chain status
|
| 165 |
+
"""
|
| 166 |
+
# Get count of logs
|
| 167 |
+
query = select(AuditLog).where(AuditLog.tenant_id == tenant_id)
|
| 168 |
+
result = await db.execute(query)
|
| 169 |
+
logs = list(result.scalars().all())
|
| 170 |
+
|
| 171 |
+
if not logs:
|
| 172 |
+
return {
|
| 173 |
+
"total_entries": 0,
|
| 174 |
+
"chain_valid": True,
|
| 175 |
+
"issues": [],
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
# Verify chain
|
| 179 |
+
is_valid, issues = await cls.verify_chain(db, tenant_id)
|
| 180 |
+
|
| 181 |
+
return {
|
| 182 |
+
"total_entries": len(logs),
|
| 183 |
+
"chain_valid": is_valid,
|
| 184 |
+
"issues": issues,
|
| 185 |
+
"first_entry_timestamp": logs[0].timestamp.isoformat() if logs else None,
|
| 186 |
+
"last_entry_timestamp": logs[-1].timestamp.isoformat() if logs else None,
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
# Convenience functions
|
| 191 |
+
async def verify_audit_chain(tenant_id: uuid.UUID) -> Dict[str, Any]:
|
| 192 |
+
"""
|
| 193 |
+
Verify the audit chain for a tenant.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
tenant_id: Tenant ID
|
| 197 |
+
|
| 198 |
+
Returns:
|
| 199 |
+
Dictionary with verification results
|
| 200 |
+
"""
|
| 201 |
+
async with get_db_session() as session:
|
| 202 |
+
return await AuditHashChain.get_chain_status(session, tenant_id)
|
logger.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Structured Logging Framework
|
| 3 |
+
|
| 4 |
+
Provides JSON-formatted structured logging for the AegisLM backend.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
import logging
|
| 9 |
+
import sys
|
| 10 |
+
import traceback
|
| 11 |
+
from datetime import datetime, timezone
|
| 12 |
+
from enum import Enum
|
| 13 |
+
from typing import Any, Dict, Optional
|
| 14 |
+
from uuid import UUID
|
| 15 |
+
|
| 16 |
+
from backend.core.config import settings
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class LogLevel(str, Enum):
|
| 20 |
+
"""Log level enumeration."""
|
| 21 |
+
|
| 22 |
+
DEBUG = "DEBUG"
|
| 23 |
+
INFO = "INFO"
|
| 24 |
+
WARNING = "WARNING"
|
| 25 |
+
ERROR = "ERROR"
|
| 26 |
+
CRITICAL = "CRITICAL"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class StructuredLogger:
|
| 30 |
+
"""
|
| 31 |
+
Structured JSON logger for AegisLM.
|
| 32 |
+
|
| 33 |
+
All logs are emitted as JSON with a consistent schema.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(
|
| 37 |
+
self,
|
| 38 |
+
name: str,
|
| 39 |
+
run_id: Optional[UUID] = None,
|
| 40 |
+
component: Optional[str] = None,
|
| 41 |
+
):
|
| 42 |
+
self.logger = logging.getLogger(name)
|
| 43 |
+
self.run_id = run_id
|
| 44 |
+
self.component = component or name
|
| 45 |
+
|
| 46 |
+
# Configure logger if not already configured
|
| 47 |
+
if not self.logger.handlers:
|
| 48 |
+
self._configure_logger()
|
| 49 |
+
|
| 50 |
+
def _configure_logger(self) -> None:
|
| 51 |
+
"""Configure the logger with JSON formatter."""
|
| 52 |
+
# Set level from settings
|
| 53 |
+
level = getattr(logging, settings.log_level.upper(), logging.INFO)
|
| 54 |
+
self.logger.setLevel(level)
|
| 55 |
+
|
| 56 |
+
# Create handler
|
| 57 |
+
handler = logging.StreamHandler(sys.stdout)
|
| 58 |
+
handler.setLevel(level)
|
| 59 |
+
|
| 60 |
+
# Use JSON formatter
|
| 61 |
+
formatter = JSONFormatter()
|
| 62 |
+
handler.setFormatter(formatter)
|
| 63 |
+
|
| 64 |
+
self.logger.addHandler(handler)
|
| 65 |
+
self.logger.propagate = False
|
| 66 |
+
|
| 67 |
+
def _build_log_entry(
|
| 68 |
+
self,
|
| 69 |
+
level: str,
|
| 70 |
+
message: str,
|
| 71 |
+
extra_metadata: Optional[Dict[str, Any]] = None,
|
| 72 |
+
exception: Optional[Exception] = None,
|
| 73 |
+
) -> Dict[str, Any]:
|
| 74 |
+
"""Build structured log entry."""
|
| 75 |
+
log_entry = {
|
| 76 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 77 |
+
"level": level,
|
| 78 |
+
"run_id": str(self.run_id) if self.run_id else None,
|
| 79 |
+
"component": self.component,
|
| 80 |
+
"message": message,
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
# Add metadata
|
| 84 |
+
if extra_metadata:
|
| 85 |
+
log_entry["metadata"] = extra_metadata
|
| 86 |
+
|
| 87 |
+
# Add exception info if provided
|
| 88 |
+
if exception:
|
| 89 |
+
log_entry["exception"] = {
|
| 90 |
+
"type": type(exception).__name__,
|
| 91 |
+
"message": str(exception),
|
| 92 |
+
"traceback": traceback.format_exc(),
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
return log_entry
|
| 96 |
+
|
| 97 |
+
def log(
|
| 98 |
+
self,
|
| 99 |
+
level: str,
|
| 100 |
+
message: str,
|
| 101 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 102 |
+
exception: Optional[Exception] = None,
|
| 103 |
+
) -> None:
|
| 104 |
+
"""Emit a log entry."""
|
| 105 |
+
log_entry = self._build_log_entry(level, message, metadata, exception)
|
| 106 |
+
|
| 107 |
+
# Use standard logging
|
| 108 |
+
log_method = getattr(self.logger, level.lower(), self.logger.info)
|
| 109 |
+
log_method(json.dumps(log_entry))
|
| 110 |
+
|
| 111 |
+
def debug(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
| 112 |
+
"""Log debug message."""
|
| 113 |
+
self.log(LogLevel.DEBUG.value, message, metadata)
|
| 114 |
+
|
| 115 |
+
def info(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
| 116 |
+
"""Log info message."""
|
| 117 |
+
self.log(LogLevel.INFO.value, message, metadata)
|
| 118 |
+
|
| 119 |
+
def warning(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
| 120 |
+
"""Log warning message."""
|
| 121 |
+
self.log(LogLevel.WARNING.value, message, metadata)
|
| 122 |
+
|
| 123 |
+
def error(
|
| 124 |
+
self,
|
| 125 |
+
message: str,
|
| 126 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 127 |
+
exception: Optional[Exception] = None,
|
| 128 |
+
) -> None:
|
| 129 |
+
"""Log error message."""
|
| 130 |
+
self.log(LogLevel.ERROR.value, message, metadata, exception)
|
| 131 |
+
|
| 132 |
+
def critical(
|
| 133 |
+
self,
|
| 134 |
+
message: str,
|
| 135 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 136 |
+
exception: Optional[Exception] = None,
|
| 137 |
+
) -> None:
|
| 138 |
+
"""Log critical message."""
|
| 139 |
+
self.log(LogLevel.CRITICAL.value, message, metadata, exception)
|
| 140 |
+
|
| 141 |
+
def set_run_id(self, run_id: UUID) -> None:
|
| 142 |
+
"""Set the current run ID for all subsequent logs."""
|
| 143 |
+
self.run_id = run_id
|
| 144 |
+
|
| 145 |
+
def bind(self, **kwargs) -> "StructuredLogger":
|
| 146 |
+
"""
|
| 147 |
+
Create a new logger with additional bound context.
|
| 148 |
+
|
| 149 |
+
Returns a new logger instance with merged metadata.
|
| 150 |
+
"""
|
| 151 |
+
new_logger = StructuredLogger(
|
| 152 |
+
name=self.logger.name,
|
| 153 |
+
run_id=self.run_id,
|
| 154 |
+
component=self.component,
|
| 155 |
+
)
|
| 156 |
+
return new_logger
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class JSONFormatter(logging.Formatter):
|
| 160 |
+
"""JSON formatter for structured logging."""
|
| 161 |
+
|
| 162 |
+
def format(self, record: logging.LogRecord) -> str:
|
| 163 |
+
"""Format log record as JSON."""
|
| 164 |
+
try:
|
| 165 |
+
# Try to parse the message as JSON
|
| 166 |
+
log_data = json.loads(record.getMessage())
|
| 167 |
+
except (json.JSONDecodeError, ValueError):
|
| 168 |
+
# If not JSON, use as-is
|
| 169 |
+
log_data = {"message": record.getMessage()}
|
| 170 |
+
|
| 171 |
+
# Add standard fields
|
| 172 |
+
log_entry = {
|
| 173 |
+
"timestamp": datetime.fromtimestamp(
|
| 174 |
+
record.created, tz=timezone.utc
|
| 175 |
+
).isoformat(),
|
| 176 |
+
"level": record.levelname,
|
| 177 |
+
"logger": record.name,
|
| 178 |
+
**log_data,
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
# Add extra fields from record
|
| 182 |
+
if hasattr(record, "run_id") and record.run_id:
|
| 183 |
+
log_entry["run_id"] = record.run_id
|
| 184 |
+
|
| 185 |
+
if hasattr(record, "component") and record.component:
|
| 186 |
+
log_entry["component"] = record.component
|
| 187 |
+
|
| 188 |
+
if hasattr(record, "metadata") and record.metadata:
|
| 189 |
+
log_entry["metadata"] = record.metadata
|
| 190 |
+
|
| 191 |
+
# Add exception info
|
| 192 |
+
if record.exc_info:
|
| 193 |
+
log_entry["exception"] = {
|
| 194 |
+
"type": record.exc_info[0].__name__ if record.exc_info[0] else None,
|
| 195 |
+
"message": str(record.exc_info[1]) if record.exc_info[1] else None,
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
return json.dumps(log_entry)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
# =============================================================================
|
| 202 |
+
# Logger factory functions
|
| 203 |
+
# =============================================================================
|
| 204 |
+
|
| 205 |
+
def get_logger(
|
| 206 |
+
name: str,
|
| 207 |
+
run_id: Optional[UUID] = None,
|
| 208 |
+
component: Optional[str] = None,
|
| 209 |
+
) -> StructuredLogger:
|
| 210 |
+
"""
|
| 211 |
+
Get a structured logger instance.
|
| 212 |
+
|
| 213 |
+
Args:
|
| 214 |
+
name: Logger name (typically __name__)
|
| 215 |
+
run_id: Optional run ID for correlation
|
| 216 |
+
component: Optional component name
|
| 217 |
+
|
| 218 |
+
Returns:
|
| 219 |
+
StructuredLogger instance
|
| 220 |
+
"""
|
| 221 |
+
return StructuredLogger(name, run_id, component)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
# Default logger
|
| 225 |
+
default_logger = get_logger("aegislm")
|
schema.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Logging Schema Definitions
|
| 3 |
+
|
| 4 |
+
Defines Pydantic models for structured log entries.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from enum import Enum
|
| 9 |
+
from typing import Any, Dict, List, Optional
|
| 10 |
+
from uuid import UUID
|
| 11 |
+
|
| 12 |
+
from pydantic import BaseModel, Field
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class LogLevel(str, Enum):
|
| 16 |
+
"""Log level enumeration."""
|
| 17 |
+
|
| 18 |
+
DEBUG = "DEBUG"
|
| 19 |
+
INFO = "INFO"
|
| 20 |
+
WARNING = "WARNING"
|
| 21 |
+
ERROR = "ERROR"
|
| 22 |
+
CRITICAL = "CRITICAL"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class Component(str, Enum):
|
| 26 |
+
"""System components for logging."""
|
| 27 |
+
|
| 28 |
+
ORCHESTRATOR = "orchestrator"
|
| 29 |
+
MODEL_EXECUTOR = "model_executor"
|
| 30 |
+
ATTACKER = "attacker"
|
| 31 |
+
DEFENDER = "defender"
|
| 32 |
+
JUDGE = "judge"
|
| 33 |
+
SCORING = "scoring"
|
| 34 |
+
API = "api"
|
| 35 |
+
DATABASE = "database"
|
| 36 |
+
AGENT = "agent"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class BaseLogSchema(BaseModel):
|
| 40 |
+
"""Base schema for all log entries."""
|
| 41 |
+
|
| 42 |
+
timestamp: datetime = Field(
|
| 43 |
+
default_factory=datetime.utcnow,
|
| 44 |
+
description="ISO 8601 timestamp of the log entry"
|
| 45 |
+
)
|
| 46 |
+
level: LogLevel = Field(
|
| 47 |
+
description="Log level"
|
| 48 |
+
)
|
| 49 |
+
run_id: Optional[UUID] = Field(
|
| 50 |
+
default=None,
|
| 51 |
+
description="Evaluation run ID for correlation"
|
| 52 |
+
)
|
| 53 |
+
component: Component = Field(
|
| 54 |
+
description="Component that generated the log"
|
| 55 |
+
)
|
| 56 |
+
message: str = Field(
|
| 57 |
+
description="Log message"
|
| 58 |
+
)
|
| 59 |
+
metadata: Dict[str, Any] = Field(
|
| 60 |
+
default_factory=dict,
|
| 61 |
+
description="Additional structured metadata"
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class EvaluationLogSchema(BaseLogSchema):
|
| 66 |
+
"""Schema for evaluation-related logs."""
|
| 67 |
+
|
| 68 |
+
sample_id: Optional[str] = Field(
|
| 69 |
+
default=None,
|
| 70 |
+
description="Sample ID being processed"
|
| 71 |
+
)
|
| 72 |
+
attack_type: Optional[str] = Field(
|
| 73 |
+
default=None,
|
| 74 |
+
description="Type of attack applied"
|
| 75 |
+
)
|
| 76 |
+
model_name: Optional[str] = Field(
|
| 77 |
+
default=None,
|
| 78 |
+
description="Model being evaluated"
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class MetricsLogSchema(BaseLogSchema):
|
| 83 |
+
"""Schema for metrics and observability logs."""
|
| 84 |
+
|
| 85 |
+
latency_ms: Optional[float] = Field(
|
| 86 |
+
default=None,
|
| 87 |
+
description="Processing latency in milliseconds"
|
| 88 |
+
)
|
| 89 |
+
tokens_generated: Optional[int] = Field(
|
| 90 |
+
default=None,
|
| 91 |
+
description="Number of tokens generated"
|
| 92 |
+
)
|
| 93 |
+
attack_chain_depth: Optional[int] = Field(
|
| 94 |
+
default=None,
|
| 95 |
+
description="Depth of attack chain"
|
| 96 |
+
)
|
| 97 |
+
throughput_samples_per_sec: Optional[float] = Field(
|
| 98 |
+
default=None,
|
| 99 |
+
description="Processing throughput"
|
| 100 |
+
)
|
| 101 |
+
memory_usage_mb: Optional[float] = Field(
|
| 102 |
+
default=None,
|
| 103 |
+
description="Memory usage in MB"
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class ErrorLogSchema(BaseLogSchema):
|
| 108 |
+
"""Schema for error logs."""
|
| 109 |
+
|
| 110 |
+
error_type: str = Field(
|
| 111 |
+
description="Type of error"
|
| 112 |
+
)
|
| 113 |
+
error_message: str = Field(
|
| 114 |
+
description="Error message"
|
| 115 |
+
)
|
| 116 |
+
stack_trace: Optional[str] = Field(
|
| 117 |
+
default=None,
|
| 118 |
+
description="Stack trace if available"
|
| 119 |
+
)
|
| 120 |
+
recoverable: bool = Field(
|
| 121 |
+
default=True,
|
| 122 |
+
description="Whether the error is recoverable"
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class LifecycleLogSchema(BaseLogSchema):
|
| 127 |
+
"""Schema for lifecycle event logs."""
|
| 128 |
+
|
| 129 |
+
event: str = Field(
|
| 130 |
+
description="Lifecycle event name"
|
| 131 |
+
)
|
| 132 |
+
previous_state: Optional[str] = Field(
|
| 133 |
+
default=None,
|
| 134 |
+
description="Previous state"
|
| 135 |
+
)
|
| 136 |
+
new_state: str = Field(
|
| 137 |
+
description="New state"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
# =============================================================================
|
| 142 |
+
# Log entry schemas for specific components
|
| 143 |
+
# =============================================================================
|
| 144 |
+
|
| 145 |
+
class OrchestratorLogSchema(EvaluationLogSchema):
|
| 146 |
+
"""Schema for orchestrator logs."""
|
| 147 |
+
|
| 148 |
+
total_samples: Optional[int] = Field(
|
| 149 |
+
default=None,
|
| 150 |
+
description="Total samples to process"
|
| 151 |
+
)
|
| 152 |
+
processed_samples: Optional[int] = Field(
|
| 153 |
+
default=None,
|
| 154 |
+
description="Samples processed so far"
|
| 155 |
+
)
|
| 156 |
+
failed_samples: Optional[int] = Field(
|
| 157 |
+
default=None,
|
| 158 |
+
description="Number of failed samples"
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class ModelExecutorLogSchema(EvaluationLogSchema):
|
| 163 |
+
"""Schema for model executor logs."""
|
| 164 |
+
|
| 165 |
+
model_name: str = Field(
|
| 166 |
+
description="Model name"
|
| 167 |
+
)
|
| 168 |
+
model_version: str = Field(
|
| 169 |
+
description="Model version"
|
| 170 |
+
)
|
| 171 |
+
generation_time_ms: float = Field(
|
| 172 |
+
description="Time taken for generation"
|
| 173 |
+
)
|
| 174 |
+
input_tokens: int = Field(
|
| 175 |
+
description="Number of input tokens"
|
| 176 |
+
)
|
| 177 |
+
output_tokens: int = Field(
|
| 178 |
+
description="Number of output tokens"
|
| 179 |
+
)
|
| 180 |
+
device: str = Field(
|
| 181 |
+
description="Device used for inference"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
class ScoringLogSchema(BaseLogSchema):
|
| 186 |
+
"""Schema for scoring logs."""
|
| 187 |
+
|
| 188 |
+
hallucination_score: Optional[float] = Field(
|
| 189 |
+
default=None,
|
| 190 |
+
description="Hallucination score [0, 1]"
|
| 191 |
+
)
|
| 192 |
+
toxicity_score: Optional[float] = Field(
|
| 193 |
+
default=None,
|
| 194 |
+
description="Toxicity score [0, 1]"
|
| 195 |
+
)
|
| 196 |
+
bias_score: Optional[float] = Field(
|
| 197 |
+
default=None,
|
| 198 |
+
description="Bias score [0, 1]"
|
| 199 |
+
)
|
| 200 |
+
confidence_score: Optional[float] = Field(
|
| 201 |
+
default=None,
|
| 202 |
+
description="Confidence score [0, 1]"
|
| 203 |
+
)
|
| 204 |
+
composite_score: Optional[float] = Field(
|
| 205 |
+
default=None,
|
| 206 |
+
description="Composite robustness score"
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class APILogSchema(BaseLogSchema):
|
| 211 |
+
"""Schema for API request/response logs."""
|
| 212 |
+
|
| 213 |
+
method: str = Field(
|
| 214 |
+
description="HTTP method"
|
| 215 |
+
)
|
| 216 |
+
path: str = Field(
|
| 217 |
+
description="Request path"
|
| 218 |
+
)
|
| 219 |
+
status_code: Optional[int] = Field(
|
| 220 |
+
default=None,
|
| 221 |
+
description="Response status code"
|
| 222 |
+
)
|
| 223 |
+
request_id: Optional[str] = Field(
|
| 224 |
+
default=None,
|
| 225 |
+
description="Request ID for tracing"
|
| 226 |
+
)
|
| 227 |
+
user_agent: Optional[str] = Field(
|
| 228 |
+
default=None,
|
| 229 |
+
description="User agent string"
|
| 230 |
+
)
|
| 231 |
+
response_time_ms: Optional[float] = Field(
|
| 232 |
+
default=None,
|
| 233 |
+
description="Response time in milliseconds"
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# =============================================================================
|
| 238 |
+
# Log aggregation schemas
|
| 239 |
+
# =============================================================================
|
| 240 |
+
|
| 241 |
+
class AggregatedMetricsSchema(BaseModel):
|
| 242 |
+
"""Schema for aggregated evaluation metrics."""
|
| 243 |
+
|
| 244 |
+
run_id: UUID = Field(
|
| 245 |
+
description="Evaluation run ID"
|
| 246 |
+
)
|
| 247 |
+
total_samples: int = Field(
|
| 248 |
+
description="Total number of samples"
|
| 249 |
+
)
|
| 250 |
+
successful_samples: int = Field(
|
| 251 |
+
description="Number of successfully processed samples"
|
| 252 |
+
)
|
| 253 |
+
failed_samples: int = Field(
|
| 254 |
+
description="Number of failed samples"
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
# Aggregate scores
|
| 258 |
+
mean_hallucination: Optional[float] = Field(
|
| 259 |
+
default=None,
|
| 260 |
+
description="Mean hallucination score"
|
| 261 |
+
)
|
| 262 |
+
mean_toxicity: Optional[float] = Field(
|
| 263 |
+
default=None,
|
| 264 |
+
description="Mean toxicity score"
|
| 265 |
+
)
|
| 266 |
+
mean_bias: Optional[float] = Field(
|
| 267 |
+
default=None,
|
| 268 |
+
description="Mean bias score"
|
| 269 |
+
)
|
| 270 |
+
mean_confidence: Optional[float] = Field(
|
| 271 |
+
default=None,
|
| 272 |
+
description="Mean confidence score"
|
| 273 |
+
)
|
| 274 |
+
mean_robustness: Optional[float] = Field(
|
| 275 |
+
default=None,
|
| 276 |
+
description="Mean robustness score"
|
| 277 |
+
)
|
| 278 |
+
composite_score: Optional[float] = Field(
|
| 279 |
+
default=None,
|
| 280 |
+
description="Final composite score"
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
# Timing metrics
|
| 284 |
+
total_processing_time_ms: float = Field(
|
| 285 |
+
description="Total processing time in milliseconds"
|
| 286 |
+
)
|
| 287 |
+
mean_sample_time_ms: float = Field(
|
| 288 |
+
description="Mean time per sample in milliseconds"
|
| 289 |
+
)
|
| 290 |
+
throughput_samples_per_sec: float = Field(
|
| 291 |
+
description="Processing throughput"
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# Error summary
|
| 295 |
+
error_types: Dict[str, int] = Field(
|
| 296 |
+
default_factory=dict,
|
| 297 |
+
description="Count of each error type"
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
class RunSummarySchema(BaseModel):
|
| 302 |
+
"""Schema for evaluation run summary."""
|
| 303 |
+
|
| 304 |
+
run_id: UUID = Field(
|
| 305 |
+
description="Evaluation run ID"
|
| 306 |
+
)
|
| 307 |
+
model_name: str = Field(
|
| 308 |
+
description="Model name"
|
| 309 |
+
)
|
| 310 |
+
dataset_version: str = Field(
|
| 311 |
+
description="Dataset version"
|
| 312 |
+
)
|
| 313 |
+
status: str = Field(
|
| 314 |
+
description="Run status"
|
| 315 |
+
)
|
| 316 |
+
metrics: AggregatedMetricsSchema = Field(
|
| 317 |
+
description="Aggregated metrics"
|
| 318 |
+
)
|
| 319 |
+
artifacts_path: str = Field(
|
| 320 |
+
description="Path to run artifacts"
|
| 321 |
+
)
|
| 322 |
+
completed_at: datetime = Field(
|
| 323 |
+
description="Completion timestamp"
|
| 324 |
+
)
|
system_metrics.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
System Metrics Monitoring Module
|
| 3 |
+
|
| 4 |
+
Provides system metrics logging for performance monitoring:
|
| 5 |
+
- RAM usage
|
| 6 |
+
- GPU usage
|
| 7 |
+
- Active tasks
|
| 8 |
+
- DB connections
|
| 9 |
+
|
| 10 |
+
Logs periodically to track system health and performance.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import asyncio
|
| 14 |
+
import psutil
|
| 15 |
+
import time
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
from typing import Dict, Optional
|
| 18 |
+
|
| 19 |
+
from backend.logging.logger import get_logger
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
logger = get_logger("system_metrics", component="monitoring")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class SystemMetrics:
|
| 26 |
+
"""
|
| 27 |
+
System metrics collector for monitoring resource usage.
|
| 28 |
+
|
| 29 |
+
Tracks:
|
| 30 |
+
- RAM usage
|
| 31 |
+
- GPU memory usage
|
| 32 |
+
- Active tasks
|
| 33 |
+
- DB connections
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def __init__(self, interval: int = 30):
|
| 37 |
+
"""
|
| 38 |
+
Initialize system metrics collector.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
interval: Logging interval in seconds
|
| 42 |
+
"""
|
| 43 |
+
self.interval = interval
|
| 44 |
+
self._running = False
|
| 45 |
+
self._task: Optional[asyncio.Task] = None
|
| 46 |
+
self._last_metrics: Dict = {}
|
| 47 |
+
|
| 48 |
+
async def start(self) -> None:
|
| 49 |
+
"""Start periodic metrics collection."""
|
| 50 |
+
if self._running:
|
| 51 |
+
return
|
| 52 |
+
|
| 53 |
+
self._running = True
|
| 54 |
+
self._task = asyncio.create_task(self._collect_loop())
|
| 55 |
+
logger.info("System metrics collection started", interval=self.interval)
|
| 56 |
+
|
| 57 |
+
async def stop(self) -> None:
|
| 58 |
+
"""Stop periodic metrics collection."""
|
| 59 |
+
self._running = False
|
| 60 |
+
if self._task:
|
| 61 |
+
self._task.cancel()
|
| 62 |
+
try:
|
| 63 |
+
await self._task
|
| 64 |
+
except asyncio.CancelledError:
|
| 65 |
+
pass
|
| 66 |
+
logger.info("System metrics collection stopped")
|
| 67 |
+
|
| 68 |
+
async def _collect_loop(self) -> None:
|
| 69 |
+
"""Main collection loop."""
|
| 70 |
+
while self._running:
|
| 71 |
+
try:
|
| 72 |
+
metrics = await self.collect_metrics()
|
| 73 |
+
self._last_metrics = metrics
|
| 74 |
+
self._log_metrics(metrics)
|
| 75 |
+
except Exception as e:
|
| 76 |
+
logger.error(
|
| 77 |
+
"Failed to collect metrics",
|
| 78 |
+
error=str(e),
|
| 79 |
+
exception=e
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
await asyncio.sleep(self.interval)
|
| 83 |
+
|
| 84 |
+
async def collect_metrics(self) -> Dict:
|
| 85 |
+
"""
|
| 86 |
+
Collect current system metrics.
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
Dictionary with current metrics
|
| 90 |
+
"""
|
| 91 |
+
# CPU metrics
|
| 92 |
+
cpu_percent = psutil.cpu_percent(interval=0.1)
|
| 93 |
+
cpu_count = psutil.cpu_count()
|
| 94 |
+
|
| 95 |
+
# Memory metrics
|
| 96 |
+
memory = psutil.virtual_memory()
|
| 97 |
+
|
| 98 |
+
# Disk metrics
|
| 99 |
+
disk = psutil.disk_usage('/')
|
| 100 |
+
|
| 101 |
+
metrics = {
|
| 102 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 103 |
+
"cpu": {
|
| 104 |
+
"percent": cpu_percent,
|
| 105 |
+
"count": cpu_count,
|
| 106 |
+
},
|
| 107 |
+
"memory": {
|
| 108 |
+
"total_mb": memory.total / (1024 * 1024),
|
| 109 |
+
"available_mb": memory.available / (1024 * 1024),
|
| 110 |
+
"used_mb": memory.used / (1024 * 1024),
|
| 111 |
+
"percent": memory.percent,
|
| 112 |
+
},
|
| 113 |
+
"disk": {
|
| 114 |
+
"total_gb": disk.total / (1024 * 1024 * 1024),
|
| 115 |
+
"used_gb": disk.used / (1024 * 1024 * 1024),
|
| 116 |
+
"free_gb": disk.free / (1024 * 1024 * 1024),
|
| 117 |
+
"percent": disk.percent,
|
| 118 |
+
},
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
# GPU metrics (if available)
|
| 122 |
+
try:
|
| 123 |
+
import torch
|
| 124 |
+
if torch.cuda.is_available():
|
| 125 |
+
gpu_metrics = {
|
| 126 |
+
"available": True,
|
| 127 |
+
"device_count": torch.cuda.device_count(),
|
| 128 |
+
"current_device": torch.cuda.current_device(),
|
| 129 |
+
"memory_allocated_mb": torch.cuda.memory_allocated() / (1024 * 1024),
|
| 130 |
+
"memory_reserved_mb": torch.cuda.memory_reserved() / (1024 * 1024),
|
| 131 |
+
"max_memory_allocated_mb": torch.cuda.max_memory_allocated() / (1024 * 1024),
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
# Get memory for each device
|
| 135 |
+
device_memory = []
|
| 136 |
+
for i in range(torch.cuda.device_count()):
|
| 137 |
+
device_memory.append({
|
| 138 |
+
"device": i,
|
| 139 |
+
"allocated_mb": torch.cuda.memory_allocated(i) / (1024 * 1024),
|
| 140 |
+
"reserved_mb": torch.cuda.memory_reserved(i) / (1024 * 1024),
|
| 141 |
+
})
|
| 142 |
+
gpu_metrics["devices"] = device_memory
|
| 143 |
+
|
| 144 |
+
metrics["gpu"] = gpu_metrics
|
| 145 |
+
else:
|
| 146 |
+
metrics["gpu"] = {"available": False}
|
| 147 |
+
except ImportError:
|
| 148 |
+
metrics["gpu"] = {"available": False, "error": "PyTorch not available"}
|
| 149 |
+
|
| 150 |
+
# Active tasks (if running in async context)
|
| 151 |
+
try:
|
| 152 |
+
loop = asyncio.get_event_loop()
|
| 153 |
+
tasks = [t for t in asyncio.all_tasks(loop) if not t.done()]
|
| 154 |
+
metrics["async"] = {
|
| 155 |
+
"active_tasks": len(tasks),
|
| 156 |
+
}
|
| 157 |
+
except RuntimeError:
|
| 158 |
+
metrics["async"] = {"active_tasks": 0}
|
| 159 |
+
|
| 160 |
+
return metrics
|
| 161 |
+
|
| 162 |
+
def _log_metrics(self, metrics: Dict) -> None:
|
| 163 |
+
"""Log metrics to structured logger."""
|
| 164 |
+
# Log at info level for regular metrics
|
| 165 |
+
logger.info(
|
| 166 |
+
"System metrics snapshot",
|
| 167 |
+
memory_mb=metrics["memory"]["used_mb"],
|
| 168 |
+
memory_percent=metrics["memory"]["percent"],
|
| 169 |
+
cpu_percent=metrics["cpu"]["percent"],
|
| 170 |
+
active_tasks=metrics["async"]["active_tasks"],
|
| 171 |
+
metadata=metrics,
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
# Log warnings for high usage
|
| 175 |
+
if metrics["memory"]["percent"] > 90:
|
| 176 |
+
logger.warning(
|
| 177 |
+
"High memory usage detected",
|
| 178 |
+
memory_percent=metrics["memory"]["percent"],
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
if metrics["cpu"]["percent"] > 90:
|
| 182 |
+
logger.warning(
|
| 183 |
+
"High CPU usage detected",
|
| 184 |
+
cpu_percent=metrics["cpu"]["percent"],
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# Check GPU if available
|
| 188 |
+
if metrics.get("gpu", {}).get("available"):
|
| 189 |
+
gpu = metrics["gpu"]
|
| 190 |
+
if gpu.get("memory_allocated_mb", 0) > 0:
|
| 191 |
+
# Log GPU memory if being used
|
| 192 |
+
logger.info(
|
| 193 |
+
"GPU memory usage",
|
| 194 |
+
gpu_memory_mb=gpu["memory_allocated_mb"],
|
| 195 |
+
gpu_memory_reserved_mb=gpu["memory_reserved_mb"],
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
def get_current_metrics(self) -> Dict:
|
| 199 |
+
"""
|
| 200 |
+
Get the last collected metrics.
|
| 201 |
+
|
| 202 |
+
Returns:
|
| 203 |
+
Last metrics dictionary or empty dict if not collected yet
|
| 204 |
+
"""
|
| 205 |
+
return self._last_metrics
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# Global metrics collector instance
|
| 209 |
+
_metrics_collector: Optional[SystemMetrics] = None
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def get_metrics_collector(interval: int = 30) -> SystemMetrics:
|
| 213 |
+
"""
|
| 214 |
+
Get the global metrics collector instance.
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
interval: Collection interval in seconds
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
SystemMetrics instance
|
| 221 |
+
"""
|
| 222 |
+
global _metrics_collector
|
| 223 |
+
if _metrics_collector is None:
|
| 224 |
+
_metrics_collector = SystemMetrics(interval=interval)
|
| 225 |
+
return _metrics_collector
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
async def start_metrics_collection(interval: int = 30) -> None:
|
| 229 |
+
"""
|
| 230 |
+
Start system metrics collection.
|
| 231 |
+
|
| 232 |
+
Args:
|
| 233 |
+
interval: Collection interval in seconds
|
| 234 |
+
"""
|
| 235 |
+
collector = get_metrics_collector(interval)
|
| 236 |
+
await collector.start()
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
async def stop_metrics_collection() -> None:
|
| 240 |
+
"""Stop system metrics collection."""
|
| 241 |
+
collector = get_metrics_collector()
|
| 242 |
+
await collector.stop()
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def get_current_metrics() -> Dict:
|
| 246 |
+
"""
|
| 247 |
+
Get current system metrics.
|
| 248 |
+
|
| 249 |
+
Returns:
|
| 250 |
+
Current metrics dictionary
|
| 251 |
+
"""
|
| 252 |
+
collector = get_metrics_collector()
|
| 253 |
+
return collector.get_current_metrics()
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
__all__ = [
|
| 257 |
+
"SystemMetrics",
|
| 258 |
+
"get_metrics_collector",
|
| 259 |
+
"start_metrics_collection",
|
| 260 |
+
"stop_metrics_collection",
|
| 261 |
+
"get_current_metrics",
|
| 262 |
+
]
|