adamops 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adamops/__init__.py +40 -0
- adamops/cli.py +163 -0
- adamops/data/__init__.py +24 -0
- adamops/data/feature_engineering.py +284 -0
- adamops/data/loaders.py +922 -0
- adamops/data/preprocessors.py +227 -0
- adamops/data/splitters.py +218 -0
- adamops/data/validators.py +148 -0
- adamops/deployment/__init__.py +21 -0
- adamops/deployment/api.py +237 -0
- adamops/deployment/cloud.py +191 -0
- adamops/deployment/containerize.py +262 -0
- adamops/deployment/exporters.py +148 -0
- adamops/evaluation/__init__.py +24 -0
- adamops/evaluation/comparison.py +133 -0
- adamops/evaluation/explainability.py +143 -0
- adamops/evaluation/metrics.py +233 -0
- adamops/evaluation/reports.py +165 -0
- adamops/evaluation/visualization.py +238 -0
- adamops/models/__init__.py +21 -0
- adamops/models/automl.py +277 -0
- adamops/models/ensembles.py +228 -0
- adamops/models/modelops.py +308 -0
- adamops/models/registry.py +250 -0
- adamops/monitoring/__init__.py +21 -0
- adamops/monitoring/alerts.py +200 -0
- adamops/monitoring/dashboard.py +117 -0
- adamops/monitoring/drift.py +212 -0
- adamops/monitoring/performance.py +195 -0
- adamops/pipelines/__init__.py +15 -0
- adamops/pipelines/orchestrators.py +183 -0
- adamops/pipelines/workflows.py +212 -0
- adamops/utils/__init__.py +18 -0
- adamops/utils/config.py +457 -0
- adamops/utils/helpers.py +663 -0
- adamops/utils/logging.py +412 -0
- adamops-0.1.0.dist-info/METADATA +310 -0
- adamops-0.1.0.dist-info/RECORD +42 -0
- adamops-0.1.0.dist-info/WHEEL +5 -0
- adamops-0.1.0.dist-info/entry_points.txt +2 -0
- adamops-0.1.0.dist-info/licenses/LICENSE +21 -0
- adamops-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AdamOps Model Registry Module
|
|
3
|
+
|
|
4
|
+
Provides model versioning, metadata tracking, and comparison.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sqlite3
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
import joblib
|
|
13
|
+
|
|
14
|
+
from adamops.utils.logging import get_logger
|
|
15
|
+
from adamops.utils.config import get_config
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ModelVersion:
|
|
21
|
+
"""Represents a model version."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, name: str, version: str, path: str, metadata: Dict):
|
|
24
|
+
self.name = name
|
|
25
|
+
self.version = version
|
|
26
|
+
self.path = path
|
|
27
|
+
self.metadata = metadata
|
|
28
|
+
self.created_at = metadata.get("created_at", datetime.now().isoformat())
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> Dict:
|
|
31
|
+
return {
|
|
32
|
+
"name": self.name, "version": self.version, "path": self.path,
|
|
33
|
+
"metadata": self.metadata, "created_at": self.created_at
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_dict(cls, data: Dict) -> "ModelVersion":
|
|
38
|
+
return cls(data["name"], data["version"], data["path"], data["metadata"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ModelRegistry:
|
|
42
|
+
"""Model registry for versioning and tracking."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, path: Optional[str] = None, backend: str = "json"):
|
|
45
|
+
"""
|
|
46
|
+
Initialize registry.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
path: Registry storage path.
|
|
50
|
+
backend: 'json' or 'sqlite'.
|
|
51
|
+
"""
|
|
52
|
+
config = get_config()
|
|
53
|
+
self.path = Path(path or config.registry_path)
|
|
54
|
+
self.backend = backend
|
|
55
|
+
self.path.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
if backend == "sqlite":
|
|
58
|
+
self._init_sqlite()
|
|
59
|
+
else:
|
|
60
|
+
self._init_json()
|
|
61
|
+
|
|
62
|
+
def _init_json(self):
|
|
63
|
+
self.registry_file = self.path / "registry.json"
|
|
64
|
+
if not self.registry_file.exists():
|
|
65
|
+
self._save_json({})
|
|
66
|
+
|
|
67
|
+
def _init_sqlite(self):
|
|
68
|
+
self.db_file = self.path / "registry.db"
|
|
69
|
+
conn = sqlite3.connect(self.db_file)
|
|
70
|
+
conn.execute("""
|
|
71
|
+
CREATE TABLE IF NOT EXISTS models (
|
|
72
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
73
|
+
name TEXT, version TEXT, path TEXT,
|
|
74
|
+
metadata TEXT, created_at TEXT,
|
|
75
|
+
UNIQUE(name, version)
|
|
76
|
+
)
|
|
77
|
+
""")
|
|
78
|
+
conn.commit()
|
|
79
|
+
conn.close()
|
|
80
|
+
|
|
81
|
+
def _load_json(self) -> Dict:
|
|
82
|
+
with open(self.registry_file) as f:
|
|
83
|
+
return json.load(f)
|
|
84
|
+
|
|
85
|
+
def _save_json(self, data: Dict):
|
|
86
|
+
with open(self.registry_file, "w") as f:
|
|
87
|
+
json.dump(data, f, indent=2)
|
|
88
|
+
|
|
89
|
+
def register(self, name: str, model: Any, metadata: Optional[Dict] = None,
|
|
90
|
+
version: Optional[str] = None) -> ModelVersion:
|
|
91
|
+
"""
|
|
92
|
+
Register a model.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
name: Model name.
|
|
96
|
+
model: Model object to save.
|
|
97
|
+
metadata: Model metadata (algorithm, metrics, etc).
|
|
98
|
+
version: Version string (auto-incremented if None).
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
ModelVersion: Registered model version.
|
|
102
|
+
"""
|
|
103
|
+
# Get next version
|
|
104
|
+
if version is None:
|
|
105
|
+
existing = self.list_versions(name)
|
|
106
|
+
version = f"v{len(existing) + 1}"
|
|
107
|
+
|
|
108
|
+
# Save model
|
|
109
|
+
model_path = self.path / "models" / name / f"{version}.joblib"
|
|
110
|
+
model_path.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
joblib.dump(model, model_path)
|
|
112
|
+
|
|
113
|
+
# Create version record
|
|
114
|
+
meta = metadata or {}
|
|
115
|
+
meta["created_at"] = datetime.now().isoformat()
|
|
116
|
+
model_version = ModelVersion(name, version, str(model_path), meta)
|
|
117
|
+
|
|
118
|
+
# Store in registry
|
|
119
|
+
if self.backend == "json":
|
|
120
|
+
data = self._load_json()
|
|
121
|
+
if name not in data:
|
|
122
|
+
data[name] = {}
|
|
123
|
+
data[name][version] = model_version.to_dict()
|
|
124
|
+
self._save_json(data)
|
|
125
|
+
else:
|
|
126
|
+
conn = sqlite3.connect(self.db_file)
|
|
127
|
+
conn.execute(
|
|
128
|
+
"INSERT OR REPLACE INTO models (name, version, path, metadata, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
129
|
+
(name, version, str(model_path), json.dumps(meta), meta["created_at"])
|
|
130
|
+
)
|
|
131
|
+
conn.commit()
|
|
132
|
+
conn.close()
|
|
133
|
+
|
|
134
|
+
logger.info(f"Registered model {name} version {version}")
|
|
135
|
+
return model_version
|
|
136
|
+
|
|
137
|
+
def get(self, name: str, version: str = "latest") -> Optional[ModelVersion]:
|
|
138
|
+
"""Get a model version."""
|
|
139
|
+
if version == "latest":
|
|
140
|
+
versions = self.list_versions(name)
|
|
141
|
+
if not versions:
|
|
142
|
+
return None
|
|
143
|
+
version = versions[-1].version
|
|
144
|
+
|
|
145
|
+
if self.backend == "json":
|
|
146
|
+
data = self._load_json()
|
|
147
|
+
if name in data and version in data[name]:
|
|
148
|
+
return ModelVersion.from_dict(data[name][version])
|
|
149
|
+
else:
|
|
150
|
+
conn = sqlite3.connect(self.db_file)
|
|
151
|
+
cur = conn.execute(
|
|
152
|
+
"SELECT name, version, path, metadata FROM models WHERE name=? AND version=?",
|
|
153
|
+
(name, version)
|
|
154
|
+
)
|
|
155
|
+
row = cur.fetchone()
|
|
156
|
+
conn.close()
|
|
157
|
+
if row:
|
|
158
|
+
return ModelVersion(row[0], row[1], row[2], json.loads(row[3]))
|
|
159
|
+
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
def load(self, name: str, version: str = "latest") -> Any:
|
|
163
|
+
"""Load a model."""
|
|
164
|
+
model_version = self.get(name, version)
|
|
165
|
+
if model_version is None:
|
|
166
|
+
raise ValueError(f"Model {name} version {version} not found")
|
|
167
|
+
return joblib.load(model_version.path)
|
|
168
|
+
|
|
169
|
+
def list_versions(self, name: str) -> List[ModelVersion]:
|
|
170
|
+
"""List all versions of a model."""
|
|
171
|
+
if self.backend == "json":
|
|
172
|
+
data = self._load_json()
|
|
173
|
+
if name not in data:
|
|
174
|
+
return []
|
|
175
|
+
return [ModelVersion.from_dict(v) for v in data[name].values()]
|
|
176
|
+
else:
|
|
177
|
+
conn = sqlite3.connect(self.db_file)
|
|
178
|
+
cur = conn.execute(
|
|
179
|
+
"SELECT name, version, path, metadata FROM models WHERE name=? ORDER BY created_at",
|
|
180
|
+
(name,)
|
|
181
|
+
)
|
|
182
|
+
versions = [ModelVersion(r[0], r[1], r[2], json.loads(r[3])) for r in cur.fetchall()]
|
|
183
|
+
conn.close()
|
|
184
|
+
return versions
|
|
185
|
+
|
|
186
|
+
def list_models(self) -> List[str]:
|
|
187
|
+
"""List all registered model names."""
|
|
188
|
+
if self.backend == "json":
|
|
189
|
+
return list(self._load_json().keys())
|
|
190
|
+
else:
|
|
191
|
+
conn = sqlite3.connect(self.db_file)
|
|
192
|
+
cur = conn.execute("SELECT DISTINCT name FROM models")
|
|
193
|
+
names = [r[0] for r in cur.fetchall()]
|
|
194
|
+
conn.close()
|
|
195
|
+
return names
|
|
196
|
+
|
|
197
|
+
def compare(self, name: str, metric: str = "accuracy") -> Dict:
|
|
198
|
+
"""Compare all versions of a model by a metric."""
|
|
199
|
+
versions = self.list_versions(name)
|
|
200
|
+
comparison = []
|
|
201
|
+
for v in versions:
|
|
202
|
+
metrics = v.metadata.get("metrics", {})
|
|
203
|
+
comparison.append({
|
|
204
|
+
"version": v.version,
|
|
205
|
+
"created_at": v.created_at,
|
|
206
|
+
metric: metrics.get(metric),
|
|
207
|
+
**{k: val for k, val in metrics.items() if k != metric}
|
|
208
|
+
})
|
|
209
|
+
return sorted(comparison, key=lambda x: x.get(metric, 0) or 0, reverse=True)
|
|
210
|
+
|
|
211
|
+
def delete(self, name: str, version: Optional[str] = None):
|
|
212
|
+
"""Delete a model or version."""
|
|
213
|
+
if version:
|
|
214
|
+
model_version = self.get(name, version)
|
|
215
|
+
if model_version and Path(model_version.path).exists():
|
|
216
|
+
Path(model_version.path).unlink()
|
|
217
|
+
|
|
218
|
+
if self.backend == "json":
|
|
219
|
+
data = self._load_json()
|
|
220
|
+
if name in data and version in data[name]:
|
|
221
|
+
del data[name][version]
|
|
222
|
+
self._save_json(data)
|
|
223
|
+
else:
|
|
224
|
+
conn = sqlite3.connect(self.db_file)
|
|
225
|
+
conn.execute("DELETE FROM models WHERE name=? AND version=?", (name, version))
|
|
226
|
+
conn.commit()
|
|
227
|
+
conn.close()
|
|
228
|
+
else:
|
|
229
|
+
# Delete all versions
|
|
230
|
+
for v in self.list_versions(name):
|
|
231
|
+
self.delete(name, v.version)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# Global registry instance
|
|
235
|
+
_registry: Optional[ModelRegistry] = None
|
|
236
|
+
|
|
237
|
+
def get_registry() -> ModelRegistry:
|
|
238
|
+
"""Get global registry instance."""
|
|
239
|
+
global _registry
|
|
240
|
+
if _registry is None:
|
|
241
|
+
_registry = ModelRegistry()
|
|
242
|
+
return _registry
|
|
243
|
+
|
|
244
|
+
def register_model(name: str, model: Any, **kwargs) -> ModelVersion:
|
|
245
|
+
"""Register a model in the global registry."""
|
|
246
|
+
return get_registry().register(name, model, **kwargs)
|
|
247
|
+
|
|
248
|
+
def load_model(name: str, version: str = "latest") -> Any:
|
|
249
|
+
"""Load a model from the global registry."""
|
|
250
|
+
return get_registry().load(name, version)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AdamOps Monitoring Module
|
|
3
|
+
|
|
4
|
+
Provides model monitoring capabilities:
|
|
5
|
+
- drift: Detect data and concept drift
|
|
6
|
+
- performance: Track model performance metrics
|
|
7
|
+
- alerts: Set up alerting for performance degradation
|
|
8
|
+
- dashboard: Create monitoring dashboards
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from adamops.monitoring import drift
|
|
12
|
+
from adamops.monitoring import performance
|
|
13
|
+
from adamops.monitoring import alerts
|
|
14
|
+
from adamops.monitoring import dashboard
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"drift",
|
|
18
|
+
"performance",
|
|
19
|
+
"alerts",
|
|
20
|
+
"dashboard",
|
|
21
|
+
]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AdamOps Alerts Module
|
|
3
|
+
|
|
4
|
+
Alerting for model performance degradation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
from adamops.utils.logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AlertSeverity(Enum):
|
|
17
|
+
INFO = "info"
|
|
18
|
+
WARNING = "warning"
|
|
19
|
+
CRITICAL = "critical"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Alert:
|
|
23
|
+
"""Represents an alert."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, name: str, message: str, severity: AlertSeverity,
|
|
26
|
+
metadata: Optional[Dict] = None):
|
|
27
|
+
self.name = name
|
|
28
|
+
self.message = message
|
|
29
|
+
self.severity = severity
|
|
30
|
+
self.metadata = metadata or {}
|
|
31
|
+
self.timestamp = datetime.now().isoformat()
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> Dict:
|
|
34
|
+
return {
|
|
35
|
+
"name": self.name,
|
|
36
|
+
"message": self.message,
|
|
37
|
+
"severity": self.severity.value,
|
|
38
|
+
"timestamp": self.timestamp,
|
|
39
|
+
"metadata": self.metadata,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AlertRule:
|
|
44
|
+
"""Defines an alert rule."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, name: str, condition: Callable[[Dict], bool],
|
|
47
|
+
message_template: str, severity: AlertSeverity = AlertSeverity.WARNING):
|
|
48
|
+
self.name = name
|
|
49
|
+
self.condition = condition
|
|
50
|
+
self.message_template = message_template
|
|
51
|
+
self.severity = severity
|
|
52
|
+
|
|
53
|
+
def check(self, data: Dict) -> Optional[Alert]:
|
|
54
|
+
"""Check if alert should be triggered."""
|
|
55
|
+
if self.condition(data):
|
|
56
|
+
message = self.message_template.format(**data)
|
|
57
|
+
return Alert(self.name, message, self.severity, data)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AlertManager:
|
|
62
|
+
"""Manage alerts and notifications."""
|
|
63
|
+
|
|
64
|
+
def __init__(self):
|
|
65
|
+
self.rules: List[AlertRule] = []
|
|
66
|
+
self.handlers: List[Callable[[Alert], None]] = []
|
|
67
|
+
self.alert_history: List[Alert] = []
|
|
68
|
+
|
|
69
|
+
def add_rule(self, rule: AlertRule):
|
|
70
|
+
"""Add an alert rule."""
|
|
71
|
+
self.rules.append(rule)
|
|
72
|
+
logger.info(f"Added alert rule: {rule.name}")
|
|
73
|
+
|
|
74
|
+
def add_handler(self, handler: Callable[[Alert], None]):
|
|
75
|
+
"""Add an alert handler (callback)."""
|
|
76
|
+
self.handlers.append(handler)
|
|
77
|
+
|
|
78
|
+
def check(self, data: Dict) -> List[Alert]:
|
|
79
|
+
"""Check all rules against data."""
|
|
80
|
+
triggered = []
|
|
81
|
+
|
|
82
|
+
for rule in self.rules:
|
|
83
|
+
alert = rule.check(data)
|
|
84
|
+
if alert:
|
|
85
|
+
triggered.append(alert)
|
|
86
|
+
self._handle_alert(alert)
|
|
87
|
+
|
|
88
|
+
return triggered
|
|
89
|
+
|
|
90
|
+
def _handle_alert(self, alert: Alert):
|
|
91
|
+
"""Handle triggered alert."""
|
|
92
|
+
self.alert_history.append(alert)
|
|
93
|
+
|
|
94
|
+
# Log alert
|
|
95
|
+
log_method = logger.warning if alert.severity == AlertSeverity.WARNING else \
|
|
96
|
+
logger.critical if alert.severity == AlertSeverity.CRITICAL else logger.info
|
|
97
|
+
log_method(f"[{alert.severity.value.upper()}] {alert.name}: {alert.message}")
|
|
98
|
+
|
|
99
|
+
# Call handlers
|
|
100
|
+
for handler in self.handlers:
|
|
101
|
+
try:
|
|
102
|
+
handler(alert)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Alert handler error: {e}")
|
|
105
|
+
|
|
106
|
+
def get_history(self, severity: Optional[AlertSeverity] = None) -> List[Alert]:
|
|
107
|
+
"""Get alert history."""
|
|
108
|
+
if severity:
|
|
109
|
+
return [a for a in self.alert_history if a.severity == severity]
|
|
110
|
+
return self.alert_history
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# Default alert rules
|
|
114
|
+
def accuracy_drop_rule(threshold: float = 0.1) -> AlertRule:
|
|
115
|
+
"""Alert rule for accuracy drop."""
|
|
116
|
+
return AlertRule(
|
|
117
|
+
name="accuracy_drop",
|
|
118
|
+
condition=lambda d: d.get("accuracy_change", 0) < -threshold,
|
|
119
|
+
message_template="Accuracy dropped by {accuracy_change:.1%}",
|
|
120
|
+
severity=AlertSeverity.WARNING,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def drift_detected_rule() -> AlertRule:
|
|
125
|
+
"""Alert rule for drift detection."""
|
|
126
|
+
return AlertRule(
|
|
127
|
+
name="drift_detected",
|
|
128
|
+
condition=lambda d: d.get("drift_detected", False),
|
|
129
|
+
message_template="Data drift detected in {drifted_columns} columns",
|
|
130
|
+
severity=AlertSeverity.WARNING,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def high_latency_rule(threshold_ms: float = 1000) -> AlertRule:
|
|
135
|
+
"""Alert rule for high latency."""
|
|
136
|
+
return AlertRule(
|
|
137
|
+
name="high_latency",
|
|
138
|
+
condition=lambda d: d.get("latency_ms", 0) > threshold_ms,
|
|
139
|
+
message_template="High latency detected: {latency_ms:.0f}ms",
|
|
140
|
+
severity=AlertSeverity.WARNING,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Notification handlers
|
|
145
|
+
def console_handler(alert: Alert):
|
|
146
|
+
"""Print alert to console."""
|
|
147
|
+
print(f"[ALERT] {alert.severity.value.upper()}: {alert.message}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def email_handler(recipients: List[str], smtp_config: Dict):
|
|
151
|
+
"""Create email alert handler."""
|
|
152
|
+
def handler(alert: Alert):
|
|
153
|
+
try:
|
|
154
|
+
import smtplib
|
|
155
|
+
from email.message import EmailMessage
|
|
156
|
+
|
|
157
|
+
msg = EmailMessage()
|
|
158
|
+
msg['Subject'] = f"[{alert.severity.value.upper()}] {alert.name}"
|
|
159
|
+
msg['From'] = smtp_config.get('from', 'alerts@adamops.local')
|
|
160
|
+
msg['To'] = ', '.join(recipients)
|
|
161
|
+
msg.set_content(f"{alert.message}\n\nTimestamp: {alert.timestamp}")
|
|
162
|
+
|
|
163
|
+
with smtplib.SMTP(smtp_config['host'], smtp_config.get('port', 587)) as s:
|
|
164
|
+
if smtp_config.get('use_tls'):
|
|
165
|
+
s.starttls()
|
|
166
|
+
if 'username' in smtp_config:
|
|
167
|
+
s.login(smtp_config['username'], smtp_config['password'])
|
|
168
|
+
s.send_message(msg)
|
|
169
|
+
|
|
170
|
+
logger.info(f"Sent email alert to {recipients}")
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"Failed to send email: {e}")
|
|
173
|
+
|
|
174
|
+
return handler
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def create_alert_manager(
|
|
178
|
+
rules: Optional[List[str]] = None,
|
|
179
|
+
handlers: Optional[List[str]] = None
|
|
180
|
+
) -> AlertManager:
|
|
181
|
+
"""Create alert manager with default rules."""
|
|
182
|
+
manager = AlertManager()
|
|
183
|
+
|
|
184
|
+
default_rules = {
|
|
185
|
+
"accuracy": accuracy_drop_rule(),
|
|
186
|
+
"drift": drift_detected_rule(),
|
|
187
|
+
"latency": high_latency_rule(),
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
rules = rules or list(default_rules.keys())
|
|
191
|
+
for rule_name in rules:
|
|
192
|
+
if rule_name in default_rules:
|
|
193
|
+
manager.add_rule(default_rules[rule_name])
|
|
194
|
+
|
|
195
|
+
handlers = handlers or ["console"]
|
|
196
|
+
for handler_name in handlers:
|
|
197
|
+
if handler_name == "console":
|
|
198
|
+
manager.add_handler(console_handler)
|
|
199
|
+
|
|
200
|
+
return manager
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AdamOps Dashboard Module
|
|
3
|
+
|
|
4
|
+
Simple monitoring dashboard.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
from adamops.utils.logging import get_logger
|
|
11
|
+
from adamops.monitoring.performance import PerformanceMonitor
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_dashboard_html(
|
|
17
|
+
monitors: Dict[str, PerformanceMonitor],
|
|
18
|
+
title: str = "AdamOps Monitoring Dashboard"
|
|
19
|
+
) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Generate HTML dashboard for monitoring.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
monitors: Dict of model names to monitors.
|
|
25
|
+
title: Dashboard title.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
HTML string.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
model_cards = ""
|
|
32
|
+
for name, monitor in monitors.items():
|
|
33
|
+
summary = monitor.summary()
|
|
34
|
+
metrics = summary.get("latest_metrics", {})
|
|
35
|
+
|
|
36
|
+
metrics_html = ""
|
|
37
|
+
for metric, value in metrics.items():
|
|
38
|
+
if isinstance(value, float):
|
|
39
|
+
metrics_html += f'<div class="metric"><span class="label">{metric}</span><span class="value">{value:.4f}</span></div>'
|
|
40
|
+
|
|
41
|
+
model_cards += f'''
|
|
42
|
+
<div class="card">
|
|
43
|
+
<h3>{name}</h3>
|
|
44
|
+
<p>Entries: {summary.get("entries", 0)}</p>
|
|
45
|
+
<p>Last update: {summary.get("latest_timestamp", "N/A")}</p>
|
|
46
|
+
<div class="metrics">{metrics_html}</div>
|
|
47
|
+
</div>
|
|
48
|
+
'''
|
|
49
|
+
|
|
50
|
+
html = f'''<!DOCTYPE html>
|
|
51
|
+
<html>
|
|
52
|
+
<head>
|
|
53
|
+
<title>{title}</title>
|
|
54
|
+
<style>
|
|
55
|
+
body {{ font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #1a1a2e; color: #eee; }}
|
|
56
|
+
h1 {{ color: #4a90d9; }}
|
|
57
|
+
.container {{ max-width: 1200px; margin: 0 auto; }}
|
|
58
|
+
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }}
|
|
59
|
+
.card {{ background: #16213e; padding: 20px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }}
|
|
60
|
+
.card h3 {{ color: #4a90d9; margin-top: 0; }}
|
|
61
|
+
.metrics {{ margin-top: 15px; }}
|
|
62
|
+
.metric {{ display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #333; }}
|
|
63
|
+
.label {{ color: #888; }}
|
|
64
|
+
.value {{ font-weight: bold; color: #4ecdc4; }}
|
|
65
|
+
.timestamp {{ color: #666; font-size: 12px; margin-top: 20px; }}
|
|
66
|
+
</style>
|
|
67
|
+
</head>
|
|
68
|
+
<body>
|
|
69
|
+
<div class="container">
|
|
70
|
+
<h1>{title}</h1>
|
|
71
|
+
<div class="grid">{model_cards}</div>
|
|
72
|
+
<p class="timestamp">Generated: {datetime.now().isoformat()}</p>
|
|
73
|
+
</div>
|
|
74
|
+
</body>
|
|
75
|
+
</html>'''
|
|
76
|
+
|
|
77
|
+
return html
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def save_dashboard(
|
|
81
|
+
monitors: Dict[str, PerformanceMonitor],
|
|
82
|
+
output_path: str,
|
|
83
|
+
title: str = "AdamOps Monitoring Dashboard"
|
|
84
|
+
):
|
|
85
|
+
"""Save dashboard to HTML file."""
|
|
86
|
+
html = generate_dashboard_html(monitors, title)
|
|
87
|
+
with open(output_path, 'w') as f:
|
|
88
|
+
f.write(html)
|
|
89
|
+
logger.info(f"Dashboard saved to {output_path}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_streamlit_dashboard(monitors: Dict[str, PerformanceMonitor]) -> str:
|
|
93
|
+
"""Generate Streamlit dashboard code."""
|
|
94
|
+
return '''"""AdamOps Monitoring Dashboard
|
|
95
|
+
Run with: streamlit run dashboard.py
|
|
96
|
+
"""
|
|
97
|
+
import streamlit as st
|
|
98
|
+
import pandas as pd
|
|
99
|
+
import plotly.express as px
|
|
100
|
+
from adamops.monitoring.performance import PerformanceMonitor
|
|
101
|
+
|
|
102
|
+
st.set_page_config(page_title="AdamOps Monitor", layout="wide")
|
|
103
|
+
st.title("AdamOps Monitoring Dashboard")
|
|
104
|
+
|
|
105
|
+
# Load monitors (customize paths)
|
|
106
|
+
# monitor = PerformanceMonitor("model_name")
|
|
107
|
+
|
|
108
|
+
# Placeholder content
|
|
109
|
+
st.subheader("Model Performance")
|
|
110
|
+
st.info("Configure monitors to see metrics")
|
|
111
|
+
|
|
112
|
+
# Example metrics display
|
|
113
|
+
col1, col2, col3 = st.columns(3)
|
|
114
|
+
col1.metric("Accuracy", "0.95", "+0.02")
|
|
115
|
+
col2.metric("Latency", "45ms", "-5ms")
|
|
116
|
+
col3.metric("Predictions", "1,234", "+100")
|
|
117
|
+
'''
|