pvw-cli 1.2.8__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.
Potentially problematic release.
This version of pvw-cli might be problematic. Click here for more details.
- purviewcli/__init__.py +27 -0
- purviewcli/__main__.py +15 -0
- purviewcli/cli/__init__.py +5 -0
- purviewcli/cli/account.py +199 -0
- purviewcli/cli/cli.py +170 -0
- purviewcli/cli/collections.py +502 -0
- purviewcli/cli/domain.py +361 -0
- purviewcli/cli/entity.py +2436 -0
- purviewcli/cli/glossary.py +533 -0
- purviewcli/cli/health.py +250 -0
- purviewcli/cli/insight.py +113 -0
- purviewcli/cli/lineage.py +1103 -0
- purviewcli/cli/management.py +141 -0
- purviewcli/cli/policystore.py +103 -0
- purviewcli/cli/relationship.py +75 -0
- purviewcli/cli/scan.py +357 -0
- purviewcli/cli/search.py +527 -0
- purviewcli/cli/share.py +478 -0
- purviewcli/cli/types.py +831 -0
- purviewcli/cli/unified_catalog.py +3540 -0
- purviewcli/cli/workflow.py +402 -0
- purviewcli/client/__init__.py +21 -0
- purviewcli/client/_account.py +1877 -0
- purviewcli/client/_collections.py +1761 -0
- purviewcli/client/_domain.py +414 -0
- purviewcli/client/_entity.py +3545 -0
- purviewcli/client/_glossary.py +3233 -0
- purviewcli/client/_health.py +501 -0
- purviewcli/client/_insight.py +2873 -0
- purviewcli/client/_lineage.py +2138 -0
- purviewcli/client/_management.py +2202 -0
- purviewcli/client/_policystore.py +2915 -0
- purviewcli/client/_relationship.py +1351 -0
- purviewcli/client/_scan.py +2607 -0
- purviewcli/client/_search.py +1472 -0
- purviewcli/client/_share.py +272 -0
- purviewcli/client/_types.py +2708 -0
- purviewcli/client/_unified_catalog.py +5112 -0
- purviewcli/client/_workflow.py +2734 -0
- purviewcli/client/api_client.py +1295 -0
- purviewcli/client/business_rules.py +675 -0
- purviewcli/client/config.py +231 -0
- purviewcli/client/data_quality.py +433 -0
- purviewcli/client/endpoint.py +123 -0
- purviewcli/client/endpoints.py +554 -0
- purviewcli/client/exceptions.py +38 -0
- purviewcli/client/lineage_visualization.py +797 -0
- purviewcli/client/monitoring_dashboard.py +712 -0
- purviewcli/client/rate_limiter.py +30 -0
- purviewcli/client/retry_handler.py +125 -0
- purviewcli/client/scanning_operations.py +523 -0
- purviewcli/client/settings.py +1 -0
- purviewcli/client/sync_client.py +250 -0
- purviewcli/plugins/__init__.py +1 -0
- purviewcli/plugins/plugin_system.py +709 -0
- pvw_cli-1.2.8.dist-info/METADATA +1618 -0
- pvw_cli-1.2.8.dist-info/RECORD +60 -0
- pvw_cli-1.2.8.dist-info/WHEEL +5 -0
- pvw_cli-1.2.8.dist-info/entry_points.txt +3 -0
- pvw_cli-1.2.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Real-time Monitoring Dashboard for Microsoft Purview
|
|
3
|
+
Provides live monitoring, metrics collection, and alerting capabilities
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional, Any, Callable, Tuple
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
# Optional pandas dependency for report generation
|
|
18
|
+
try:
|
|
19
|
+
import pandas as pd
|
|
20
|
+
PANDAS_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
pd = None
|
|
23
|
+
PANDAS_AVAILABLE = False
|
|
24
|
+
print("Warning: pandas not available. Metrics export features will be limited.")
|
|
25
|
+
from rich.panel import Panel
|
|
26
|
+
from rich.layout import Layout
|
|
27
|
+
from rich.live import Live
|
|
28
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
29
|
+
from rich.text import Text
|
|
30
|
+
from rich.align import Align
|
|
31
|
+
import threading
|
|
32
|
+
|
|
33
|
+
from .api_client import PurviewClient, PurviewConfig
|
|
34
|
+
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
class MetricType(Enum):
|
|
38
|
+
"""Types of metrics to monitor"""
|
|
39
|
+
SCAN_STATUS = "scan_status"
|
|
40
|
+
ENTITY_COUNT = "entity_count"
|
|
41
|
+
API_PERFORMANCE = "api_performance"
|
|
42
|
+
DATA_QUALITY = "data_quality"
|
|
43
|
+
CLASSIFICATION_COVERAGE = "classification_coverage"
|
|
44
|
+
LINEAGE_COMPLETENESS = "lineage_completeness"
|
|
45
|
+
GLOSSARY_USAGE = "glossary_usage"
|
|
46
|
+
USER_ACTIVITY = "user_activity"
|
|
47
|
+
|
|
48
|
+
class AlertSeverity(Enum):
|
|
49
|
+
"""Alert severity levels"""
|
|
50
|
+
INFO = "info"
|
|
51
|
+
WARNING = "warning"
|
|
52
|
+
ERROR = "error"
|
|
53
|
+
CRITICAL = "critical"
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Metric:
|
|
57
|
+
"""Monitoring metric"""
|
|
58
|
+
name: str
|
|
59
|
+
value: Any
|
|
60
|
+
timestamp: datetime
|
|
61
|
+
metric_type: MetricType
|
|
62
|
+
tags: Dict[str, str] = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Alert:
|
|
66
|
+
"""Monitoring alert"""
|
|
67
|
+
id: str
|
|
68
|
+
title: str
|
|
69
|
+
description: str
|
|
70
|
+
severity: AlertSeverity
|
|
71
|
+
timestamp: datetime
|
|
72
|
+
metric_name: str
|
|
73
|
+
threshold_value: Any
|
|
74
|
+
actual_value: Any
|
|
75
|
+
is_resolved: bool = False
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class Threshold:
|
|
79
|
+
"""Monitoring threshold"""
|
|
80
|
+
metric_name: str
|
|
81
|
+
operator: str # >, <, >=, <=, ==, !=
|
|
82
|
+
value: Any
|
|
83
|
+
severity: AlertSeverity
|
|
84
|
+
description: str
|
|
85
|
+
|
|
86
|
+
class MonitoringDashboard:
|
|
87
|
+
"""Real-time monitoring dashboard for Purview"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, client: PurviewClient):
|
|
90
|
+
self.client = client
|
|
91
|
+
self.console = Console()
|
|
92
|
+
self.metrics: List[Metric] = []
|
|
93
|
+
self.alerts: List[Alert] = []
|
|
94
|
+
self.thresholds: List[Threshold] = []
|
|
95
|
+
self.is_monitoring = False
|
|
96
|
+
self.monitoring_thread = None
|
|
97
|
+
self.refresh_interval = 30 # seconds
|
|
98
|
+
|
|
99
|
+
# Default thresholds
|
|
100
|
+
self._setup_default_thresholds()
|
|
101
|
+
|
|
102
|
+
def _setup_default_thresholds(self):
|
|
103
|
+
"""Setup default monitoring thresholds"""
|
|
104
|
+
self.thresholds = [
|
|
105
|
+
Threshold("failed_scans", ">=", 5, AlertSeverity.WARNING, "Multiple scan failures detected"),
|
|
106
|
+
Threshold("api_response_time", ">=", 5000, AlertSeverity.WARNING, "API response time high"),
|
|
107
|
+
Threshold("entity_count_change", "<=", -100, AlertSeverity.ERROR, "Significant entity count decrease"),
|
|
108
|
+
Threshold("classification_coverage", "<=", 50, AlertSeverity.WARNING, "Low classification coverage"),
|
|
109
|
+
Threshold("data_quality_score", "<=", 70, AlertSeverity.ERROR, "Data quality score below threshold"),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
async def collect_metrics(self) -> List[Metric]:
|
|
113
|
+
"""Collect current metrics from Purview"""
|
|
114
|
+
metrics = []
|
|
115
|
+
current_time = datetime.now()
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# Scan status metrics
|
|
119
|
+
scan_metrics = await self._collect_scan_metrics()
|
|
120
|
+
metrics.extend(scan_metrics)
|
|
121
|
+
|
|
122
|
+
# Entity count metrics
|
|
123
|
+
entity_metrics = await self._collect_entity_metrics()
|
|
124
|
+
metrics.extend(entity_metrics)
|
|
125
|
+
|
|
126
|
+
# API performance metrics
|
|
127
|
+
api_metrics = await self._collect_api_metrics()
|
|
128
|
+
metrics.extend(api_metrics)
|
|
129
|
+
|
|
130
|
+
# Classification coverage metrics
|
|
131
|
+
classification_metrics = await self._collect_classification_metrics()
|
|
132
|
+
metrics.extend(classification_metrics)
|
|
133
|
+
|
|
134
|
+
# Lineage completeness metrics
|
|
135
|
+
lineage_metrics = await self._collect_lineage_metrics()
|
|
136
|
+
metrics.extend(lineage_metrics)
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
self.console.print(f"[red]Error collecting metrics: {e}[/red]")
|
|
140
|
+
|
|
141
|
+
# Store metrics
|
|
142
|
+
self.metrics.extend(metrics)
|
|
143
|
+
|
|
144
|
+
# Keep only last 1000 metrics to prevent memory issues
|
|
145
|
+
if len(self.metrics) > 1000:
|
|
146
|
+
self.metrics = self.metrics[-1000:]
|
|
147
|
+
|
|
148
|
+
return metrics
|
|
149
|
+
|
|
150
|
+
async def _collect_scan_metrics(self) -> List[Metric]:
|
|
151
|
+
"""Collect scan-related metrics"""
|
|
152
|
+
metrics = []
|
|
153
|
+
current_time = datetime.now()
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# Get data sources
|
|
157
|
+
data_sources = await self.client._make_request('GET', '/scan/datasources')
|
|
158
|
+
|
|
159
|
+
running_scans = 0
|
|
160
|
+
failed_scans = 0
|
|
161
|
+
completed_scans = 0
|
|
162
|
+
|
|
163
|
+
for ds in data_sources.get('value', []):
|
|
164
|
+
ds_name = ds.get('name', '')
|
|
165
|
+
|
|
166
|
+
# Get scans for this data source
|
|
167
|
+
try:
|
|
168
|
+
scans_response = await self.client._make_request('GET', f'/scan/datasources/{ds_name}/scans')
|
|
169
|
+
scans = scans_response.get('value', [])
|
|
170
|
+
|
|
171
|
+
for scan in scans:
|
|
172
|
+
scan_name = scan.get('name', '')
|
|
173
|
+
|
|
174
|
+
# Get recent runs
|
|
175
|
+
try:
|
|
176
|
+
runs_response = await self.client._make_request('GET', f'/scan/datasources/{ds_name}/scans/{scan_name}/runs')
|
|
177
|
+
runs = runs_response.get('value', [])
|
|
178
|
+
|
|
179
|
+
for run in runs[-5:]: # Check last 5 runs
|
|
180
|
+
status = run.get('status', '').lower()
|
|
181
|
+
if status == 'running':
|
|
182
|
+
running_scans += 1
|
|
183
|
+
elif status == 'failed':
|
|
184
|
+
failed_scans += 1
|
|
185
|
+
elif status == 'succeeded':
|
|
186
|
+
completed_scans += 1
|
|
187
|
+
except:
|
|
188
|
+
continue
|
|
189
|
+
except:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
metrics.extend([
|
|
193
|
+
Metric("running_scans", running_scans, current_time, MetricType.SCAN_STATUS),
|
|
194
|
+
Metric("failed_scans", failed_scans, current_time, MetricType.SCAN_STATUS),
|
|
195
|
+
Metric("completed_scans", completed_scans, current_time, MetricType.SCAN_STATUS),
|
|
196
|
+
])
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
self.console.print(f"[yellow]Warning: Could not collect scan metrics: {e}[/yellow]")
|
|
200
|
+
|
|
201
|
+
return metrics
|
|
202
|
+
|
|
203
|
+
async def _collect_entity_metrics(self) -> List[Metric]:
|
|
204
|
+
"""Collect entity-related metrics"""
|
|
205
|
+
metrics = []
|
|
206
|
+
current_time = datetime.now()
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
# Get entity counts by type
|
|
210
|
+
search_payload = {
|
|
211
|
+
"keywords": "*",
|
|
212
|
+
"limit": 0, # We only want the count
|
|
213
|
+
"facets": [
|
|
214
|
+
{"facet": "entityType", "sort": {"count": "desc"}}
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
search_response = await self.client._make_request('POST', '/search/query', json=search_payload)
|
|
219
|
+
|
|
220
|
+
total_entities = search_response.get('@search.count', 0)
|
|
221
|
+
metrics.append(Metric("total_entities", total_entities, current_time, MetricType.ENTITY_COUNT))
|
|
222
|
+
|
|
223
|
+
# Entity counts by type
|
|
224
|
+
facets = search_response.get('@search.facets', {})
|
|
225
|
+
entity_type_facet = facets.get('entityType', [])
|
|
226
|
+
|
|
227
|
+
for facet in entity_type_facet:
|
|
228
|
+
entity_type = facet.get('value', 'unknown')
|
|
229
|
+
count = facet.get('count', 0)
|
|
230
|
+
metrics.append(
|
|
231
|
+
Metric(f"entities_{entity_type}", count, current_time, MetricType.ENTITY_COUNT, {"type": entity_type})
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
self.console.print(f"[yellow]Warning: Could not collect entity metrics: {e}[/yellow]")
|
|
236
|
+
|
|
237
|
+
return metrics
|
|
238
|
+
|
|
239
|
+
async def _collect_api_metrics(self) -> List[Metric]:
|
|
240
|
+
"""Collect API performance metrics"""
|
|
241
|
+
metrics = []
|
|
242
|
+
current_time = datetime.now()
|
|
243
|
+
|
|
244
|
+
# Measure API response time with a simple request
|
|
245
|
+
start_time = time.time()
|
|
246
|
+
try:
|
|
247
|
+
await self.client._make_request('GET', '/types/typedefs')
|
|
248
|
+
response_time = (time.time() - start_time) * 1000 # Convert to milliseconds
|
|
249
|
+
metrics.append(Metric("api_response_time", response_time, current_time, MetricType.API_PERFORMANCE))
|
|
250
|
+
except Exception as e:
|
|
251
|
+
response_time = -1 # Indicate failure
|
|
252
|
+
metrics.append(Metric("api_response_time", response_time, current_time, MetricType.API_PERFORMANCE))
|
|
253
|
+
|
|
254
|
+
return metrics
|
|
255
|
+
|
|
256
|
+
async def _collect_classification_metrics(self) -> List[Metric]:
|
|
257
|
+
"""Collect classification coverage metrics"""
|
|
258
|
+
metrics = []
|
|
259
|
+
current_time = datetime.now()
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
# Search for classified entities
|
|
263
|
+
classified_search = {
|
|
264
|
+
"keywords": "*",
|
|
265
|
+
"limit": 0,
|
|
266
|
+
"filter": {
|
|
267
|
+
"and": [
|
|
268
|
+
{"not": {"attributeName": "classifications", "operator": "eq", "attributeValue": None}}
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
classified_response = await self.client._make_request('POST', '/search/query', json=classified_search)
|
|
274
|
+
classified_count = classified_response.get('@search.count', 0)
|
|
275
|
+
|
|
276
|
+
# Get total entity count
|
|
277
|
+
total_search = {"keywords": "*", "limit": 0}
|
|
278
|
+
total_response = await self.client._make_request('POST', '/search/query', json=total_search)
|
|
279
|
+
total_count = total_response.get('@search.count', 0)
|
|
280
|
+
|
|
281
|
+
if total_count > 0:
|
|
282
|
+
coverage_percentage = (classified_count / total_count) * 100
|
|
283
|
+
else:
|
|
284
|
+
coverage_percentage = 0
|
|
285
|
+
|
|
286
|
+
metrics.extend([
|
|
287
|
+
Metric("classified_entities", classified_count, current_time, MetricType.CLASSIFICATION_COVERAGE),
|
|
288
|
+
Metric("classification_coverage", coverage_percentage, current_time, MetricType.CLASSIFICATION_COVERAGE),
|
|
289
|
+
])
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
self.console.print(f"[yellow]Warning: Could not collect classification metrics: {e}[/yellow]")
|
|
293
|
+
|
|
294
|
+
return metrics
|
|
295
|
+
|
|
296
|
+
async def _collect_lineage_metrics(self) -> List[Metric]:
|
|
297
|
+
"""Collect lineage completeness metrics"""
|
|
298
|
+
metrics = []
|
|
299
|
+
current_time = datetime.now()
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
# This is a simplified approach - in practice, you'd want more sophisticated lineage analysis
|
|
303
|
+
search_payload = {
|
|
304
|
+
"keywords": "*",
|
|
305
|
+
"limit": 100,
|
|
306
|
+
"filter": {
|
|
307
|
+
"entityType": "DataSet"
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
search_response = await self.client._make_request('POST', '/search/query', json=search_payload)
|
|
312
|
+
entities = search_response.get('value', [])
|
|
313
|
+
|
|
314
|
+
entities_with_lineage = 0
|
|
315
|
+
for entity in entities:
|
|
316
|
+
guid = entity.get('id', '')
|
|
317
|
+
if guid:
|
|
318
|
+
try:
|
|
319
|
+
lineage = await self.client._make_request('GET', f'/lineage/{guid}')
|
|
320
|
+
if lineage.get('relations'):
|
|
321
|
+
entities_with_lineage += 1
|
|
322
|
+
except:
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
total_datasets = len(entities)
|
|
326
|
+
if total_datasets > 0:
|
|
327
|
+
lineage_percentage = (entities_with_lineage / total_datasets) * 100
|
|
328
|
+
else:
|
|
329
|
+
lineage_percentage = 0
|
|
330
|
+
|
|
331
|
+
metrics.extend([
|
|
332
|
+
Metric("entities_with_lineage", entities_with_lineage, current_time, MetricType.LINEAGE_COMPLETENESS),
|
|
333
|
+
Metric("lineage_completeness", lineage_percentage, current_time, MetricType.LINEAGE_COMPLETENESS),
|
|
334
|
+
])
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
self.console.print(f"[yellow]Warning: Could not collect lineage metrics: {e}[/yellow]")
|
|
338
|
+
|
|
339
|
+
return metrics
|
|
340
|
+
|
|
341
|
+
def check_thresholds(self, metrics: List[Metric]) -> List[Alert]:
|
|
342
|
+
"""Check metrics against thresholds and generate alerts"""
|
|
343
|
+
new_alerts = []
|
|
344
|
+
|
|
345
|
+
for metric in metrics:
|
|
346
|
+
for threshold in self.thresholds:
|
|
347
|
+
if metric.name == threshold.metric_name:
|
|
348
|
+
violation = self._check_threshold_violation(metric.value, threshold)
|
|
349
|
+
|
|
350
|
+
if violation:
|
|
351
|
+
alert = Alert(
|
|
352
|
+
id=f"{metric.name}_{int(metric.timestamp.timestamp())}",
|
|
353
|
+
title=f"Threshold Violation: {metric.name}",
|
|
354
|
+
description=threshold.description,
|
|
355
|
+
severity=threshold.severity,
|
|
356
|
+
timestamp=metric.timestamp,
|
|
357
|
+
metric_name=metric.name,
|
|
358
|
+
threshold_value=threshold.value,
|
|
359
|
+
actual_value=metric.value
|
|
360
|
+
)
|
|
361
|
+
new_alerts.append(alert)
|
|
362
|
+
|
|
363
|
+
self.alerts.extend(new_alerts)
|
|
364
|
+
return new_alerts
|
|
365
|
+
|
|
366
|
+
def _check_threshold_violation(self, value: Any, threshold: Threshold) -> bool:
|
|
367
|
+
"""Check if a value violates a threshold"""
|
|
368
|
+
try:
|
|
369
|
+
if threshold.operator == ">":
|
|
370
|
+
return value > threshold.value
|
|
371
|
+
elif threshold.operator == ">=":
|
|
372
|
+
return value >= threshold.value
|
|
373
|
+
elif threshold.operator == "<":
|
|
374
|
+
return value < threshold.value
|
|
375
|
+
elif threshold.operator == "<=":
|
|
376
|
+
return value <= threshold.value
|
|
377
|
+
elif threshold.operator == "==":
|
|
378
|
+
return value == threshold.value
|
|
379
|
+
elif threshold.operator == "!=":
|
|
380
|
+
return value != threshold.value
|
|
381
|
+
except:
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
def create_dashboard_layout(self) -> Layout:
|
|
387
|
+
"""Create the dashboard layout"""
|
|
388
|
+
layout = Layout()
|
|
389
|
+
|
|
390
|
+
layout.split_column(
|
|
391
|
+
Layout(name="header", size=3),
|
|
392
|
+
Layout(name="body", ratio=1),
|
|
393
|
+
Layout(name="footer", size=5)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
layout["body"].split_row(
|
|
397
|
+
Layout(name="metrics", ratio=2),
|
|
398
|
+
Layout(name="alerts", ratio=1)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return layout
|
|
402
|
+
|
|
403
|
+
def update_dashboard_content(self, layout: Layout):
|
|
404
|
+
"""Update dashboard content with current data"""
|
|
405
|
+
# Header
|
|
406
|
+
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
407
|
+
header_text = Text(f"Purview Monitoring Dashboard - {current_time}", style="bold blue")
|
|
408
|
+
layout["header"].update(Panel(Align.center(header_text), border_style="blue"))
|
|
409
|
+
|
|
410
|
+
# Metrics
|
|
411
|
+
metrics_table = self._create_metrics_table()
|
|
412
|
+
layout["metrics"].update(Panel(metrics_table, title="Current Metrics", border_style="green"))
|
|
413
|
+
|
|
414
|
+
# Alerts
|
|
415
|
+
alerts_table = self._create_alerts_table()
|
|
416
|
+
layout["alerts"].update(Panel(alerts_table, title="Active Alerts", border_style="red"))
|
|
417
|
+
|
|
418
|
+
# Footer with summary
|
|
419
|
+
summary_text = self._create_summary_text()
|
|
420
|
+
layout["footer"].update(Panel(summary_text, title="Summary", border_style="yellow"))
|
|
421
|
+
|
|
422
|
+
def _create_metrics_table(self) -> Table:
|
|
423
|
+
"""Create metrics display table"""
|
|
424
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
425
|
+
table.add_column("Metric", style="cyan", no_wrap=True)
|
|
426
|
+
table.add_column("Value", style="green")
|
|
427
|
+
table.add_column("Timestamp", style="yellow")
|
|
428
|
+
|
|
429
|
+
# Get latest metrics by name
|
|
430
|
+
latest_metrics = {}
|
|
431
|
+
for metric in self.metrics:
|
|
432
|
+
if metric.name not in latest_metrics or metric.timestamp > latest_metrics[metric.name].timestamp:
|
|
433
|
+
latest_metrics[metric.name] = metric
|
|
434
|
+
|
|
435
|
+
for metric in sorted(latest_metrics.values(), key=lambda m: m.name):
|
|
436
|
+
value_str = str(metric.value)
|
|
437
|
+
if isinstance(metric.value, float):
|
|
438
|
+
value_str = f"{metric.value:.2f}"
|
|
439
|
+
|
|
440
|
+
timestamp_str = metric.timestamp.strftime("%H:%M:%S")
|
|
441
|
+
table.add_row(metric.name, value_str, timestamp_str)
|
|
442
|
+
|
|
443
|
+
return table
|
|
444
|
+
|
|
445
|
+
def _create_alerts_table(self) -> Table:
|
|
446
|
+
"""Create alerts display table"""
|
|
447
|
+
table = Table(show_header=True, header_style="bold red")
|
|
448
|
+
table.add_column("Severity", style="red", no_wrap=True)
|
|
449
|
+
table.add_column("Title", style="yellow")
|
|
450
|
+
table.add_column("Time", style="cyan")
|
|
451
|
+
|
|
452
|
+
# Show only unresolved alerts from last hour
|
|
453
|
+
recent_alerts = [
|
|
454
|
+
alert for alert in self.alerts
|
|
455
|
+
if not alert.is_resolved and alert.timestamp > datetime.now() - timedelta(hours=1)
|
|
456
|
+
]
|
|
457
|
+
|
|
458
|
+
for alert in sorted(recent_alerts, key=lambda a: a.timestamp, reverse=True)[:10]:
|
|
459
|
+
severity_style = {
|
|
460
|
+
AlertSeverity.CRITICAL: "bold red",
|
|
461
|
+
AlertSeverity.ERROR: "red",
|
|
462
|
+
AlertSeverity.WARNING: "yellow",
|
|
463
|
+
AlertSeverity.INFO: "blue"
|
|
464
|
+
}.get(alert.severity, "white")
|
|
465
|
+
|
|
466
|
+
table.add_row(
|
|
467
|
+
Text(alert.severity.value.upper(), style=severity_style),
|
|
468
|
+
alert.title,
|
|
469
|
+
alert.timestamp.strftime("%H:%M:%S")
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
return table
|
|
473
|
+
|
|
474
|
+
def _create_summary_text(self) -> Text:
|
|
475
|
+
"""Create summary text"""
|
|
476
|
+
total_alerts = len([a for a in self.alerts if not a.is_resolved])
|
|
477
|
+
critical_alerts = len([a for a in self.alerts if not a.is_resolved and a.severity == AlertSeverity.CRITICAL])
|
|
478
|
+
|
|
479
|
+
# Get key metrics
|
|
480
|
+
latest_metrics = {}
|
|
481
|
+
for metric in self.metrics:
|
|
482
|
+
if metric.name not in latest_metrics or metric.timestamp > latest_metrics[metric.name].timestamp:
|
|
483
|
+
latest_metrics[metric.name] = metric
|
|
484
|
+
|
|
485
|
+
total_entities = latest_metrics.get('total_entities', Metric('total_entities', 0, datetime.now(), MetricType.ENTITY_COUNT)).value
|
|
486
|
+
running_scans = latest_metrics.get('running_scans', Metric('running_scans', 0, datetime.now(), MetricType.SCAN_STATUS)).value
|
|
487
|
+
|
|
488
|
+
summary = Text()
|
|
489
|
+
summary.append(f"Active Alerts: {total_alerts} ({critical_alerts} critical) | ", style="bold")
|
|
490
|
+
summary.append(f"Total Entities: {total_entities} | ", style="cyan")
|
|
491
|
+
summary.append(f"Running Scans: {running_scans} | ", style="green")
|
|
492
|
+
summary.append(f"Refresh: {self.refresh_interval}s", style="yellow")
|
|
493
|
+
|
|
494
|
+
return summary
|
|
495
|
+
|
|
496
|
+
async def start_monitoring(self, refresh_interval: int = 30):
|
|
497
|
+
"""Start real-time monitoring"""
|
|
498
|
+
self.refresh_interval = refresh_interval
|
|
499
|
+
self.is_monitoring = True
|
|
500
|
+
|
|
501
|
+
layout = self.create_dashboard_layout()
|
|
502
|
+
|
|
503
|
+
with Live(layout, refresh_per_second=1, screen=True):
|
|
504
|
+
while self.is_monitoring:
|
|
505
|
+
try:
|
|
506
|
+
# Collect metrics
|
|
507
|
+
metrics = await self.collect_metrics()
|
|
508
|
+
|
|
509
|
+
# Check thresholds
|
|
510
|
+
new_alerts = self.check_thresholds(metrics)
|
|
511
|
+
|
|
512
|
+
# Update dashboard
|
|
513
|
+
self.update_dashboard_content(layout)
|
|
514
|
+
|
|
515
|
+
# Wait for next refresh
|
|
516
|
+
await asyncio.sleep(refresh_interval)
|
|
517
|
+
|
|
518
|
+
except KeyboardInterrupt:
|
|
519
|
+
self.stop_monitoring()
|
|
520
|
+
break
|
|
521
|
+
except Exception as e:
|
|
522
|
+
console.print(f"[red]Monitoring error: {e}[/red]")
|
|
523
|
+
await asyncio.sleep(5) # Wait before retrying
|
|
524
|
+
|
|
525
|
+
def stop_monitoring(self):
|
|
526
|
+
"""Stop monitoring"""
|
|
527
|
+
self.is_monitoring = False
|
|
528
|
+
console.print("[green]Monitoring stopped[/green]")
|
|
529
|
+
|
|
530
|
+
def export_metrics(self, output_path: str, format: str = 'json'):
|
|
531
|
+
"""Export collected metrics"""
|
|
532
|
+
if format.lower() == 'json':
|
|
533
|
+
metrics_data = [
|
|
534
|
+
{
|
|
535
|
+
'name': m.name,
|
|
536
|
+
'value': m.value,
|
|
537
|
+
'timestamp': m.timestamp.isoformat(),
|
|
538
|
+
'metric_type': m.metric_type.value,
|
|
539
|
+
'tags': m.tags
|
|
540
|
+
}
|
|
541
|
+
for m in self.metrics
|
|
542
|
+
]
|
|
543
|
+
|
|
544
|
+
with open(output_path, 'w') as f:
|
|
545
|
+
json.dump(metrics_data, f, indent=2)
|
|
546
|
+
|
|
547
|
+
elif format.lower() == 'csv':
|
|
548
|
+
df = pd.DataFrame([
|
|
549
|
+
{
|
|
550
|
+
'name': m.name,
|
|
551
|
+
'value': m.value,
|
|
552
|
+
'timestamp': m.timestamp,
|
|
553
|
+
'metric_type': m.metric_type.value,
|
|
554
|
+
}
|
|
555
|
+
for m in self.metrics
|
|
556
|
+
])
|
|
557
|
+
df.to_csv(output_path, index=False)
|
|
558
|
+
|
|
559
|
+
console.print(f"[green]Metrics exported to {output_path}[/green]")
|
|
560
|
+
|
|
561
|
+
def export_alerts(self, output_path: str, format: str = 'json'):
|
|
562
|
+
"""Export alerts"""
|
|
563
|
+
if format.lower() == 'json':
|
|
564
|
+
alerts_data = [
|
|
565
|
+
{
|
|
566
|
+
'id': a.id,
|
|
567
|
+
'title': a.title,
|
|
568
|
+
'description': a.description,
|
|
569
|
+
'severity': a.severity.value,
|
|
570
|
+
'timestamp': a.timestamp.isoformat(),
|
|
571
|
+
'metric_name': a.metric_name,
|
|
572
|
+
'threshold_value': a.threshold_value,
|
|
573
|
+
'actual_value': a.actual_value,
|
|
574
|
+
'is_resolved': a.is_resolved
|
|
575
|
+
}
|
|
576
|
+
for a in self.alerts
|
|
577
|
+
]
|
|
578
|
+
|
|
579
|
+
with open(output_path, 'w') as f:
|
|
580
|
+
json.dump(alerts_data, f, indent=2)
|
|
581
|
+
|
|
582
|
+
console.print(f"[green]Alerts exported to {output_path}[/green]")
|
|
583
|
+
|
|
584
|
+
class MonitoringReports:
|
|
585
|
+
"""Generate monitoring reports and analytics"""
|
|
586
|
+
|
|
587
|
+
def __init__(self, dashboard: MonitoringDashboard):
|
|
588
|
+
self.dashboard = dashboard
|
|
589
|
+
self.console = Console()
|
|
590
|
+
|
|
591
|
+
def generate_daily_report(self, output_path: str):
|
|
592
|
+
"""Generate daily monitoring report"""
|
|
593
|
+
now = datetime.now()
|
|
594
|
+
yesterday = now - timedelta(days=1)
|
|
595
|
+
|
|
596
|
+
# Filter metrics from last 24 hours
|
|
597
|
+
daily_metrics = [
|
|
598
|
+
m for m in self.dashboard.metrics
|
|
599
|
+
if m.timestamp >= yesterday
|
|
600
|
+
]
|
|
601
|
+
|
|
602
|
+
# Filter alerts from last 24 hours
|
|
603
|
+
daily_alerts = [
|
|
604
|
+
a for a in self.dashboard.alerts
|
|
605
|
+
if a.timestamp >= yesterday
|
|
606
|
+
]
|
|
607
|
+
|
|
608
|
+
report = {
|
|
609
|
+
'report_date': now.isoformat(),
|
|
610
|
+
'period': '24 hours',
|
|
611
|
+
'summary': {
|
|
612
|
+
'total_metrics_collected': len(daily_metrics),
|
|
613
|
+
'total_alerts_generated': len(daily_alerts),
|
|
614
|
+
'critical_alerts': len([a for a in daily_alerts if a.severity == AlertSeverity.CRITICAL]),
|
|
615
|
+
'error_alerts': len([a for a in daily_alerts if a.severity == AlertSeverity.ERROR]),
|
|
616
|
+
'warning_alerts': len([a for a in daily_alerts if a.severity == AlertSeverity.WARNING])
|
|
617
|
+
},
|
|
618
|
+
'metrics_analysis': self._analyze_metrics(daily_metrics),
|
|
619
|
+
'alerts_analysis': self._analyze_alerts(daily_alerts),
|
|
620
|
+
'recommendations': self._generate_recommendations(daily_metrics, daily_alerts)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
with open(output_path, 'w') as f:
|
|
624
|
+
json.dump(report, f, indent=2)
|
|
625
|
+
|
|
626
|
+
self.console.print(f"[green]Daily report generated: {output_path}[/green]")
|
|
627
|
+
return report
|
|
628
|
+
|
|
629
|
+
def _analyze_metrics(self, metrics: List[Metric]) -> Dict:
|
|
630
|
+
"""Analyze metrics for patterns and trends"""
|
|
631
|
+
analysis = {}
|
|
632
|
+
|
|
633
|
+
# Group metrics by name
|
|
634
|
+
metrics_by_name = {}
|
|
635
|
+
for metric in metrics:
|
|
636
|
+
if metric.name not in metrics_by_name:
|
|
637
|
+
metrics_by_name[metric.name] = []
|
|
638
|
+
metrics_by_name[metric.name].append(metric)
|
|
639
|
+
|
|
640
|
+
for name, metric_list in metrics_by_name.items():
|
|
641
|
+
values = [m.value for m in metric_list if isinstance(m.value, (int, float))]
|
|
642
|
+
if values:
|
|
643
|
+
analysis[name] = {
|
|
644
|
+
'count': len(values),
|
|
645
|
+
'min': min(values),
|
|
646
|
+
'max': max(values),
|
|
647
|
+
'avg': sum(values) / len(values),
|
|
648
|
+
'trend': 'stable' # Simplified trend analysis
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return analysis
|
|
652
|
+
|
|
653
|
+
def _analyze_alerts(self, alerts: List[Alert]) -> Dict:
|
|
654
|
+
"""Analyze alerts for patterns"""
|
|
655
|
+
if not alerts:
|
|
656
|
+
return {'total': 0}
|
|
657
|
+
|
|
658
|
+
analysis = {
|
|
659
|
+
'total': len(alerts),
|
|
660
|
+
'by_severity': {},
|
|
661
|
+
'by_metric': {},
|
|
662
|
+
'most_common_issues': []
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
# Group by severity
|
|
666
|
+
for alert in alerts:
|
|
667
|
+
severity = alert.severity.value
|
|
668
|
+
if severity not in analysis['by_severity']:
|
|
669
|
+
analysis['by_severity'][severity] = 0
|
|
670
|
+
analysis['by_severity'][severity] += 1
|
|
671
|
+
|
|
672
|
+
# Group by metric
|
|
673
|
+
for alert in alerts:
|
|
674
|
+
metric = alert.metric_name
|
|
675
|
+
if metric not in analysis['by_metric']:
|
|
676
|
+
analysis['by_metric'][metric] = 0
|
|
677
|
+
analysis['by_metric'][metric] += 1
|
|
678
|
+
|
|
679
|
+
# Find most common issues
|
|
680
|
+
metric_counts = sorted(analysis['by_metric'].items(), key=lambda x: x[1], reverse=True)
|
|
681
|
+
analysis['most_common_issues'] = metric_counts[:5]
|
|
682
|
+
|
|
683
|
+
return analysis
|
|
684
|
+
|
|
685
|
+
def _generate_recommendations(self, metrics: List[Metric], alerts: List[Alert]) -> List[str]:
|
|
686
|
+
"""Generate recommendations based on metrics and alerts"""
|
|
687
|
+
recommendations = []
|
|
688
|
+
|
|
689
|
+
# Analyze alert patterns
|
|
690
|
+
critical_alerts = [a for a in alerts if a.severity == AlertSeverity.CRITICAL]
|
|
691
|
+
if critical_alerts:
|
|
692
|
+
recommendations.append("Address critical alerts immediately to prevent system issues")
|
|
693
|
+
|
|
694
|
+
# Check scan failure patterns
|
|
695
|
+
failed_scan_alerts = [a for a in alerts if 'failed_scans' in a.metric_name]
|
|
696
|
+
if failed_scan_alerts:
|
|
697
|
+
recommendations.append("Review scan configurations and data source connectivity")
|
|
698
|
+
|
|
699
|
+
# Check API performance
|
|
700
|
+
api_alerts = [a for a in alerts if 'api_response_time' in a.metric_name]
|
|
701
|
+
if api_alerts:
|
|
702
|
+
recommendations.append("Monitor API performance and consider scaling if needed")
|
|
703
|
+
|
|
704
|
+
# Check classification coverage
|
|
705
|
+
classification_alerts = [a for a in alerts if 'classification_coverage' in a.metric_name]
|
|
706
|
+
if classification_alerts:
|
|
707
|
+
recommendations.append("Improve data classification coverage through automated scanning")
|
|
708
|
+
|
|
709
|
+
if not recommendations:
|
|
710
|
+
recommendations.append("System is performing well - continue monitoring")
|
|
711
|
+
|
|
712
|
+
return recommendations
|