ACA050 commited on
Commit
7343c19
·
verified ·
1 Parent(s): 2a7f7e3

Upload folder using huggingface_hub

Browse files
__init__.py CHANGED
@@ -1,96 +1,47 @@
1
  """
2
- Monitoring Module
3
 
4
- Continuous AI Governance Infrastructure for AegisLM.
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 .alerting import (
29
- AlertManager,
30
- AlertSeverity,
31
- AlertSummary,
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 .pipeline import (
42
- MonitoringPipeline,
43
- MonitoringConfig,
44
- MonitoringDashboardData,
45
- get_monitoring_pipeline,
46
- )
47
- from .schemas import (
48
- Alert,
49
- AlertSeverity,
50
- AlertSummary,
51
- AlertType,
52
- DriftDetectionResult,
53
- MonitoringConfig,
54
- MonitoringDashboardData,
55
- MonitoringRequest,
56
- MonitoringResponse,
57
- RollingMetrics,
58
- )
59
- from .streaming_evaluator import (
60
- StreamingEvaluator,
61
- get_streaming_evaluator,
62
  )
63
 
64
  __all__ = [
65
- # Pipeline
66
- "MonitoringPipeline",
67
- "MonitoringConfig",
68
- "MonitoringDashboardData",
69
- "get_monitoring_pipeline",
70
-
71
- # Streaming Evaluator
72
- "StreamingEvaluator",
73
- "get_streaming_evaluator",
74
-
75
- # Drift Detection
76
- "DriftDetector",
77
- "DriftDetectionResult",
78
- "MetricWindow",
79
- "get_drift_detector",
80
-
81
- # Alerting
82
- "AlertManager",
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
+ ]