Ahmedik95316 commited on
Commit
291cac4
·
1 Parent(s): 0591093

Create model_registry.py

Browse files

Adding Blue-Green Deployment Strategy

Files changed (1) hide show
  1. deployment/model_registry.py +671 -0
deployment/model_registry.py ADDED
@@ -0,0 +1,671 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import joblib
3
+ import logging
4
+ import hashlib
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from datetime import datetime, timedelta
8
+ from typing import Dict, List, Optional, Any, Tuple
9
+ from dataclasses import dataclass, asdict
10
+
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class ModelStatus(Enum):
15
+ TRAINING = "training"
16
+ VALIDATING = "validating"
17
+ STAGED = "staged"
18
+ ACTIVE = "active"
19
+ RETIRED = "retired"
20
+ FAILED = "failed"
21
+
22
+ @dataclass
23
+ class ModelMetadata:
24
+ """Comprehensive model metadata"""
25
+ version_id: str
26
+ name: str
27
+ description: str
28
+ created_at: str
29
+ created_by: str
30
+ status: str
31
+
32
+ # Model files
33
+ model_path: str
34
+ vectorizer_path: str
35
+ pipeline_path: Optional[str]
36
+
37
+ # Performance metrics
38
+ training_metrics: Dict[str, float]
39
+ validation_metrics: Dict[str, float]
40
+ cross_validation_results: Dict[str, Any]
41
+
42
+ # Training details
43
+ training_config: Dict[str, Any]
44
+ dataset_info: Dict[str, Any]
45
+ feature_info: Dict[str, Any]
46
+
47
+ # Deployment info
48
+ deployment_history: List[Dict[str, Any]]
49
+ performance_history: List[Dict[str, Any]]
50
+
51
+ # Model signature
52
+ model_signature: str
53
+ dependencies: Dict[str, str]
54
+
55
+ # Tags and labels
56
+ tags: List[str]
57
+ labels: Dict[str, str]
58
+
59
+ class ModelRegistry:
60
+ """Central registry for managing model versions and metadata"""
61
+
62
+ def __init__(self, base_dir: Path = None):
63
+ self.base_dir = base_dir or Path("/tmp")
64
+ self.setup_registry_paths()
65
+ self.setup_registry_config()
66
+
67
+ # Model storage
68
+ self.models = {} # version_id -> ModelMetadata
69
+ self.load_registry()
70
+
71
+ def setup_registry_paths(self):
72
+ """Setup model registry paths"""
73
+ self.registry_dir = self.base_dir / "registry"
74
+ self.registry_dir.mkdir(parents=True, exist_ok=True)
75
+
76
+ # Registry files
77
+ self.registry_index_path = self.registry_dir / "model_index.json"
78
+ self.registry_metadata_path = self.registry_dir / "registry_metadata.json"
79
+ self.registry_log_path = self.registry_dir / "registry_log.json"
80
+
81
+ # Model storage directory
82
+ self.models_storage_dir = self.registry_dir / "models"
83
+ self.models_storage_dir.mkdir(parents=True, exist_ok=True)
84
+
85
+ def setup_registry_config(self):
86
+ """Setup registry configuration"""
87
+ self.registry_config = {
88
+ 'max_versions_per_model': 10,
89
+ 'auto_cleanup_enabled': True,
90
+ 'cleanup_after_days': 30,
91
+ 'backup_enabled': True,
92
+ 'backup_interval_hours': 24,
93
+ 'validation_required': True,
94
+ 'signature_verification': True
95
+ }
96
+
97
+ def register_model(self, model_path: str, vectorizer_path: str,
98
+ metadata: Dict[str, Any], version_id: str = None) -> str:
99
+ """Register a new model version"""
100
+ try:
101
+ # Generate version ID if not provided
102
+ if not version_id:
103
+ version_id = f"v{datetime.now().strftime('%Y%m%d_%H%M%S')}"
104
+
105
+ # Validate model files exist
106
+ if not Path(model_path).exists():
107
+ raise FileNotFoundError(f"Model file not found: {model_path}")
108
+ if not Path(vectorizer_path).exists():
109
+ raise FileNotFoundError(f"Vectorizer file not found: {vectorizer_path}")
110
+
111
+ # Create model storage directory
112
+ model_storage_dir = self.models_storage_dir / version_id
113
+ model_storage_dir.mkdir(parents=True, exist_ok=True)
114
+
115
+ # Copy model files to registry storage
116
+ import shutil
117
+ registry_model_path = model_storage_dir / "model.pkl"
118
+ registry_vectorizer_path = model_storage_dir / "vectorizer.pkl"
119
+
120
+ shutil.copy2(model_path, registry_model_path)
121
+ shutil.copy2(vectorizer_path, registry_vectorizer_path)
122
+
123
+ # Generate model signature
124
+ model_signature = self.generate_model_signature(registry_model_path, registry_vectorizer_path)
125
+
126
+ # Create comprehensive metadata
127
+ model_metadata = ModelMetadata(
128
+ version_id=version_id,
129
+ name=metadata.get('name', f'model_{version_id}'),
130
+ description=metadata.get('description', 'No description provided'),
131
+ created_at=datetime.now().isoformat(),
132
+ created_by=metadata.get('created_by', 'system'),
133
+ status=ModelStatus.VALIDATING.value,
134
+
135
+ # File paths
136
+ model_path=str(registry_model_path),
137
+ vectorizer_path=str(registry_vectorizer_path),
138
+ pipeline_path=metadata.get('pipeline_path'),
139
+
140
+ # Performance metrics
141
+ training_metrics=metadata.get('training_metrics', {}),
142
+ validation_metrics=metadata.get('validation_metrics', {}),
143
+ cross_validation_results=metadata.get('cross_validation_results', {}),
144
+
145
+ # Training details
146
+ training_config=metadata.get('training_config', {}),
147
+ dataset_info=metadata.get('dataset_info', {}),
148
+ feature_info=metadata.get('feature_info', {}),
149
+
150
+ # Deployment info
151
+ deployment_history=[],
152
+ performance_history=[],
153
+
154
+ # Model signature
155
+ model_signature=model_signature,
156
+ dependencies=metadata.get('dependencies', {}),
157
+
158
+ # Tags and labels
159
+ tags=metadata.get('tags', []),
160
+ labels=metadata.get('labels', {})
161
+ )
162
+
163
+ # Validate model if required
164
+ if self.registry_config['validation_required']:
165
+ validation_result = self.validate_model(model_metadata)
166
+ if not validation_result['valid']:
167
+ model_metadata.status = ModelStatus.FAILED.value
168
+ self.log_registry_event("model_validation_failed",
169
+ f"Model validation failed: {validation_result['errors']}")
170
+ else:
171
+ model_metadata.status = ModelStatus.STAGED.value
172
+ else:
173
+ model_metadata.status = ModelStatus.STAGED.value
174
+
175
+ # Save metadata to file
176
+ metadata_file = model_storage_dir / "metadata.json"
177
+ with open(metadata_file, 'w') as f:
178
+ json.dump(asdict(model_metadata), f, indent=2)
179
+
180
+ # Register in memory
181
+ self.models[version_id] = model_metadata
182
+
183
+ # Update registry index
184
+ self.update_registry_index()
185
+
186
+ # Log registration
187
+ self.log_registry_event("model_registered", f"Registered model version {version_id}", {
188
+ 'version_id': version_id,
189
+ 'model_signature': model_signature,
190
+ 'status': model_metadata.status
191
+ })
192
+
193
+ logger.info(f"Successfully registered model version: {version_id}")
194
+ return version_id
195
+
196
+ except Exception as e:
197
+ logger.error(f"Failed to register model: {e}")
198
+ raise e
199
+
200
+ def get_model(self, version_id: str) -> Optional[ModelMetadata]:
201
+ """Get model metadata by version ID"""
202
+ return self.models.get(version_id)
203
+
204
+ def get_active_model(self) -> Optional[ModelMetadata]:
205
+ """Get currently active model"""
206
+ for model in self.models.values():
207
+ if model.status == ModelStatus.ACTIVE.value:
208
+ return model
209
+ return None
210
+
211
+ def list_models(self, status: str = None, limit: int = None) -> List[ModelMetadata]:
212
+ """List models with optional filtering"""
213
+ models = list(self.models.values())
214
+
215
+ # Filter by status
216
+ if status:
217
+ models = [m for m in models if m.status == status]
218
+
219
+ # Sort by creation date (newest first)
220
+ models.sort(key=lambda x: x.created_at, reverse=True)
221
+
222
+ # Apply limit
223
+ if limit:
224
+ models = models[:limit]
225
+
226
+ return models
227
+
228
+ def promote_model(self, version_id: str) -> bool:
229
+ """Promote a model to active status"""
230
+ try:
231
+ model = self.get_model(version_id)
232
+ if not model:
233
+ raise ValueError(f"Model {version_id} not found")
234
+
235
+ if model.status != ModelStatus.STAGED.value:
236
+ raise ValueError(f"Model {version_id} is not staged for promotion")
237
+
238
+ # Demote current active model
239
+ current_active = self.get_active_model()
240
+ if current_active:
241
+ current_active.status = ModelStatus.RETIRED.value
242
+ self.log_registry_event("model_retired", f"Retired model {current_active.version_id}")
243
+
244
+ # Promote new model
245
+ model.status = ModelStatus.ACTIVE.value
246
+
247
+ # Record deployment
248
+ deployment_record = {
249
+ 'promoted_at': datetime.now().isoformat(),
250
+ 'promoted_by': 'system',
251
+ 'previous_active': current_active.version_id if current_active else None
252
+ }
253
+ model.deployment_history.append(deployment_record)
254
+
255
+ # Update registry
256
+ self.update_registry_index()
257
+ self.save_model_metadata(model)
258
+
259
+ self.log_registry_event("model_promoted", f"Promoted model {version_id} to active", {
260
+ 'version_id': version_id,
261
+ 'previous_active': current_active.version_id if current_active else None
262
+ })
263
+
264
+ logger.info(f"Successfully promoted model {version_id} to active")
265
+ return True
266
+
267
+ except Exception as e:
268
+ logger.error(f"Failed to promote model {version_id}: {e}")
269
+ return False
270
+
271
+ def retire_model(self, version_id: str) -> bool:
272
+ """Retire a model version"""
273
+ try:
274
+ model = self.get_model(version_id)
275
+ if not model:
276
+ raise ValueError(f"Model {version_id} not found")
277
+
278
+ old_status = model.status
279
+ model.status = ModelStatus.RETIRED.value
280
+
281
+ # Update registry
282
+ self.update_registry_index()
283
+ self.save_model_metadata(model)
284
+
285
+ self.log_registry_event("model_retired", f"Retired model {version_id}", {
286
+ 'version_id': version_id,
287
+ 'previous_status': old_status
288
+ })
289
+
290
+ logger.info(f"Successfully retired model {version_id}")
291
+ return True
292
+
293
+ except Exception as e:
294
+ logger.error(f"Failed to retire model {version_id}: {e}")
295
+ return False
296
+
297
+ def delete_model(self, version_id: str, force: bool = False) -> bool:
298
+ """Delete a model version"""
299
+ try:
300
+ model = self.get_model(version_id)
301
+ if not model:
302
+ raise ValueError(f"Model {version_id} not found")
303
+
304
+ # Prevent deletion of active model unless forced
305
+ if model.status == ModelStatus.ACTIVE.value and not force:
306
+ raise ValueError("Cannot delete active model without force=True")
307
+
308
+ # Remove from memory
309
+ del self.models[version_id]
310
+
311
+ # Remove model storage directory
312
+ model_storage_dir = self.models_storage_dir / version_id
313
+ if model_storage_dir.exists():
314
+ import shutil
315
+ shutil.rmtree(model_storage_dir)
316
+
317
+ # Update registry index
318
+ self.update_registry_index()
319
+
320
+ self.log_registry_event("model_deleted", f"Deleted model {version_id}", {
321
+ 'version_id': version_id,
322
+ 'forced': force
323
+ })
324
+
325
+ logger.info(f"Successfully deleted model {version_id}")
326
+ return True
327
+
328
+ except Exception as e:
329
+ logger.error(f"Failed to delete model {version_id}: {e}")
330
+ return False
331
+
332
+ def validate_model(self, model_metadata: ModelMetadata) -> Dict[str, Any]:
333
+ """Validate a registered model"""
334
+ validation_result = {
335
+ 'valid': True,
336
+ 'errors': [],
337
+ 'warnings': []
338
+ }
339
+
340
+ try:
341
+ # Check if model files exist
342
+ if not Path(model_metadata.model_path).exists():
343
+ validation_result['errors'].append("Model file not found")
344
+ validation_result['valid'] = False
345
+
346
+ if not Path(model_metadata.vectorizer_path).exists():
347
+ validation_result['errors'].append("Vectorizer file not found")
348
+ validation_result['valid'] = False
349
+
350
+ # Try to load model
351
+ try:
352
+ model = joblib.load(model_metadata.model_path)
353
+ vectorizer = joblib.load(model_metadata.vectorizer_path)
354
+
355
+ # Check if model has required methods
356
+ if not hasattr(model, 'predict'):
357
+ validation_result['errors'].append("Model missing predict method")
358
+ validation_result['valid'] = False
359
+
360
+ if not hasattr(vectorizer, 'transform'):
361
+ validation_result['errors'].append("Vectorizer missing transform method")
362
+ validation_result['valid'] = False
363
+
364
+ # Test prediction with dummy data
365
+ try:
366
+ test_text = ["This is a test article for validation"]
367
+ X = vectorizer.transform(test_text)
368
+ prediction = model.predict(X)
369
+
370
+ if hasattr(model, 'predict_proba'):
371
+ probabilities = model.predict_proba(X)
372
+ except Exception as e:
373
+ validation_result['errors'].append(f"Model prediction test failed: {str(e)}")
374
+ validation_result['valid'] = False
375
+
376
+ except Exception as e:
377
+ validation_result['errors'].append(f"Failed to load model: {str(e)}")
378
+ validation_result['valid'] = False
379
+
380
+ # Check performance metrics
381
+ if not model_metadata.training_metrics:
382
+ validation_result['warnings'].append("No training metrics available")
383
+
384
+ # Verify signature if enabled
385
+ if self.registry_config['signature_verification']:
386
+ current_signature = self.generate_model_signature(
387
+ model_metadata.model_path,
388
+ model_metadata.vectorizer_path
389
+ )
390
+ if current_signature != model_metadata.model_signature:
391
+ validation_result['errors'].append("Model signature verification failed")
392
+ validation_result['valid'] = False
393
+
394
+ except Exception as e:
395
+ validation_result['errors'].append(f"Validation error: {str(e)}")
396
+ validation_result['valid'] = False
397
+
398
+ return validation_result
399
+
400
+ def generate_model_signature(self, model_path: str, vectorizer_path: str) -> str:
401
+ """Generate a signature for model files"""
402
+ try:
403
+ hasher = hashlib.sha256()
404
+
405
+ # Hash model file
406
+ with open(model_path, 'rb') as f:
407
+ for chunk in iter(lambda: f.read(4096), b""):
408
+ hasher.update(chunk)
409
+
410
+ # Hash vectorizer file
411
+ with open(vectorizer_path, 'rb') as f:
412
+ for chunk in iter(lambda: f.read(4096), b""):
413
+ hasher.update(chunk)
414
+
415
+ return hasher.hexdigest()
416
+
417
+ except Exception as e:
418
+ logger.error(f"Failed to generate model signature: {e}")
419
+ return ""
420
+
421
+ def record_performance(self, version_id: str, performance_metrics: Dict[str, float]):
422
+ """Record performance metrics for a model"""
423
+ try:
424
+ model = self.get_model(version_id)
425
+ if not model:
426
+ raise ValueError(f"Model {version_id} not found")
427
+
428
+ performance_record = {
429
+ 'timestamp': datetime.now().isoformat(),
430
+ 'metrics': performance_metrics
431
+ }
432
+
433
+ model.performance_history.append(performance_record)
434
+
435
+ # Keep only last 100 performance records
436
+ if len(model.performance_history) > 100:
437
+ model.performance_history = model.performance_history[-100:]
438
+
439
+ # Save updated metadata
440
+ self.save_model_metadata(model)
441
+
442
+ logger.info(f"Recorded performance for model {version_id}")
443
+
444
+ except Exception as e:
445
+ logger.error(f"Failed to record performance for model {version_id}: {e}")
446
+
447
+ def get_model_comparison(self, version_id1: str, version_id2: str) -> Dict[str, Any]:
448
+ """Compare two model versions"""
449
+ try:
450
+ model1 = self.get_model(version_id1)
451
+ model2 = self.get_model(version_id2)
452
+
453
+ if not model1 or not model2:
454
+ raise ValueError("One or both models not found")
455
+
456
+ comparison = {
457
+ 'model1': {
458
+ 'version_id': model1.version_id,
459
+ 'created_at': model1.created_at,
460
+ 'status': model1.status,
461
+ 'training_metrics': model1.training_metrics,
462
+ 'validation_metrics': model1.validation_metrics
463
+ },
464
+ 'model2': {
465
+ 'version_id': model2.version_id,
466
+ 'created_at': model2.created_at,
467
+ 'status': model2.status,
468
+ 'training_metrics': model2.training_metrics,
469
+ 'validation_metrics': model2.validation_metrics
470
+ },
471
+ 'comparison_timestamp': datetime.now().isoformat()
472
+ }
473
+
474
+ # Calculate metric differences
475
+ metric_diffs = {}
476
+ for metric in model1.training_metrics:
477
+ if metric in model2.training_metrics:
478
+ diff = model2.training_metrics[metric] - model1.training_metrics[metric]
479
+ metric_diffs[metric] = {
480
+ 'difference': diff,
481
+ 'improvement': diff > 0,
482
+ 'percentage_change': (diff / model1.training_metrics[metric]) * 100 if model1.training_metrics[metric] != 0 else 0
483
+ }
484
+
485
+ comparison['metric_differences'] = metric_diffs
486
+
487
+ return comparison
488
+
489
+ except Exception as e:
490
+ logger.error(f"Failed to compare models: {e}")
491
+ return {'error': str(e)}
492
+
493
+ def cleanup_old_models(self):
494
+ """Clean up old retired models"""
495
+ try:
496
+ if not self.registry_config['auto_cleanup_enabled']:
497
+ return
498
+
499
+ cleanup_date = datetime.now() - timedelta(days=self.registry_config['cleanup_after_days'])
500
+
501
+ models_to_cleanup = []
502
+ for model in self.models.values():
503
+ if (model.status == ModelStatus.RETIRED.value and
504
+ datetime.fromisoformat(model.created_at) < cleanup_date):
505
+ models_to_cleanup.append(model.version_id)
506
+
507
+ for version_id in models_to_cleanup:
508
+ self.delete_model(version_id, force=True)
509
+ logger.info(f"Cleaned up old model: {version_id}")
510
+
511
+ except Exception as e:
512
+ logger.error(f"Failed to cleanup old models: {e}")
513
+
514
+ def update_registry_index(self):
515
+ """Update the registry index file"""
516
+ try:
517
+ index = {
518
+ 'last_updated': datetime.now().isoformat(),
519
+ 'total_models': len(self.models),
520
+ 'models_by_status': {},
521
+ 'model_versions': []
522
+ }
523
+
524
+ # Count models by status
525
+ for model in self.models.values():
526
+ status = model.status
527
+ index['models_by_status'][status] = index['models_by_status'].get(status, 0) + 1
528
+
529
+ # Add model summaries
530
+ for model in self.models.values():
531
+ index['model_versions'].append({
532
+ 'version_id': model.version_id,
533
+ 'name': model.name,
534
+ 'status': model.status,
535
+ 'created_at': model.created_at,
536
+ 'signature': model.model_signature
537
+ })
538
+
539
+ # Save index
540
+ with open(self.registry_index_path, 'w') as f:
541
+ json.dump(index, f, indent=2)
542
+
543
+ except Exception as e:
544
+ logger.error(f"Failed to update registry index: {e}")
545
+
546
+ def save_model_metadata(self, model: ModelMetadata):
547
+ """Save model metadata to file"""
548
+ try:
549
+ model_storage_dir = self.models_storage_dir / model.version_id
550
+ metadata_file = model_storage_dir / "metadata.json"
551
+
552
+ with open(metadata_file, 'w') as f:
553
+ json.dump(asdict(model), f, indent=2)
554
+
555
+ except Exception as e:
556
+ logger.error(f"Failed to save model metadata: {e}")
557
+
558
+ def load_registry(self):
559
+ """Load registry from storage"""
560
+ try:
561
+ # Load from individual model metadata files
562
+ if self.models_storage_dir.exists():
563
+ for model_dir in self.models_storage_dir.iterdir():
564
+ if model_dir.is_dir():
565
+ metadata_file = model_dir / "metadata.json"
566
+ if metadata_file.exists():
567
+ try:
568
+ with open(metadata_file, 'r') as f:
569
+ metadata_dict = json.load(f)
570
+
571
+ model_metadata = ModelMetadata(**metadata_dict)
572
+ self.models[model_metadata.version_id] = model_metadata
573
+
574
+ except Exception as e:
575
+ logger.warning(f"Failed to load model metadata from {metadata_file}: {e}")
576
+
577
+ logger.info(f"Loaded {len(self.models)} models from registry")
578
+
579
+ except Exception as e:
580
+ logger.error(f"Failed to load registry: {e}")
581
+
582
+ def log_registry_event(self, event: str, message: str, details: Dict = None):
583
+ """Log registry events"""
584
+ try:
585
+ log_entry = {
586
+ 'timestamp': datetime.now().isoformat(),
587
+ 'event': event,
588
+ 'message': message,
589
+ 'details': details or {}
590
+ }
591
+
592
+ # Load existing logs
593
+ logs = []
594
+ if self.registry_log_path.exists():
595
+ try:
596
+ with open(self.registry_log_path, 'r') as f:
597
+ logs = json.load(f)
598
+ except:
599
+ logs = []
600
+
601
+ logs.append(log_entry)
602
+
603
+ # Keep only last 1000 entries
604
+ if len(logs) > 1000:
605
+ logs = logs[-1000:]
606
+
607
+ # Save logs
608
+ with open(self.registry_log_path, 'w') as f:
609
+ json.dump(logs, f, indent=2)
610
+
611
+ except Exception as e:
612
+ logger.error(f"Failed to log registry event: {e}")
613
+
614
+ def get_registry_stats(self) -> Dict[str, Any]:
615
+ """Get registry statistics"""
616
+ try:
617
+ stats = {
618
+ 'total_models': len(self.models),
619
+ 'models_by_status': {},
620
+ 'active_model': None,
621
+ 'latest_model': None,
622
+ 'storage_info': {},
623
+ 'recent_activity': []
624
+ }
625
+
626
+ # Count by status
627
+ for model in self.models.values():
628
+ status = model.status
629
+ stats['models_by_status'][status] = stats['models_by_status'].get(status, 0) + 1
630
+
631
+ # Get active model
632
+ active_model = self.get_active_model()
633
+ if active_model:
634
+ stats['active_model'] = {
635
+ 'version_id': active_model.version_id,
636
+ 'created_at': active_model.created_at,
637
+ 'training_metrics': active_model.training_metrics
638
+ }
639
+
640
+ # Get latest model
641
+ models_by_date = sorted(self.models.values(), key=lambda x: x.created_at, reverse=True)
642
+ if models_by_date:
643
+ latest = models_by_date[0]
644
+ stats['latest_model'] = {
645
+ 'version_id': latest.version_id,
646
+ 'created_at': latest.created_at,
647
+ 'status': latest.status
648
+ }
649
+
650
+ # Storage information
651
+ if self.models_storage_dir.exists():
652
+ total_size = sum(f.stat().st_size for f in self.models_storage_dir.rglob('*') if f.is_file())
653
+ stats['storage_info'] = {
654
+ 'total_size_mb': total_size / (1024 * 1024),
655
+ 'model_count': len(list(self.models_storage_dir.iterdir()))
656
+ }
657
+
658
+ # Recent activity
659
+ if self.registry_log_path.exists():
660
+ try:
661
+ with open(self.registry_log_path, 'r') as f:
662
+ logs = json.load(f)
663
+ stats['recent_activity'] = logs[-10:] # Last 10 events
664
+ except:
665
+ pass
666
+
667
+ return stats
668
+
669
+ except Exception as e:
670
+ logger.error(f"Failed to get registry stats: {e}")
671
+ return {'error': str(e)}