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.

Files changed (60) hide show
  1. purviewcli/__init__.py +27 -0
  2. purviewcli/__main__.py +15 -0
  3. purviewcli/cli/__init__.py +5 -0
  4. purviewcli/cli/account.py +199 -0
  5. purviewcli/cli/cli.py +170 -0
  6. purviewcli/cli/collections.py +502 -0
  7. purviewcli/cli/domain.py +361 -0
  8. purviewcli/cli/entity.py +2436 -0
  9. purviewcli/cli/glossary.py +533 -0
  10. purviewcli/cli/health.py +250 -0
  11. purviewcli/cli/insight.py +113 -0
  12. purviewcli/cli/lineage.py +1103 -0
  13. purviewcli/cli/management.py +141 -0
  14. purviewcli/cli/policystore.py +103 -0
  15. purviewcli/cli/relationship.py +75 -0
  16. purviewcli/cli/scan.py +357 -0
  17. purviewcli/cli/search.py +527 -0
  18. purviewcli/cli/share.py +478 -0
  19. purviewcli/cli/types.py +831 -0
  20. purviewcli/cli/unified_catalog.py +3540 -0
  21. purviewcli/cli/workflow.py +402 -0
  22. purviewcli/client/__init__.py +21 -0
  23. purviewcli/client/_account.py +1877 -0
  24. purviewcli/client/_collections.py +1761 -0
  25. purviewcli/client/_domain.py +414 -0
  26. purviewcli/client/_entity.py +3545 -0
  27. purviewcli/client/_glossary.py +3233 -0
  28. purviewcli/client/_health.py +501 -0
  29. purviewcli/client/_insight.py +2873 -0
  30. purviewcli/client/_lineage.py +2138 -0
  31. purviewcli/client/_management.py +2202 -0
  32. purviewcli/client/_policystore.py +2915 -0
  33. purviewcli/client/_relationship.py +1351 -0
  34. purviewcli/client/_scan.py +2607 -0
  35. purviewcli/client/_search.py +1472 -0
  36. purviewcli/client/_share.py +272 -0
  37. purviewcli/client/_types.py +2708 -0
  38. purviewcli/client/_unified_catalog.py +5112 -0
  39. purviewcli/client/_workflow.py +2734 -0
  40. purviewcli/client/api_client.py +1295 -0
  41. purviewcli/client/business_rules.py +675 -0
  42. purviewcli/client/config.py +231 -0
  43. purviewcli/client/data_quality.py +433 -0
  44. purviewcli/client/endpoint.py +123 -0
  45. purviewcli/client/endpoints.py +554 -0
  46. purviewcli/client/exceptions.py +38 -0
  47. purviewcli/client/lineage_visualization.py +797 -0
  48. purviewcli/client/monitoring_dashboard.py +712 -0
  49. purviewcli/client/rate_limiter.py +30 -0
  50. purviewcli/client/retry_handler.py +125 -0
  51. purviewcli/client/scanning_operations.py +523 -0
  52. purviewcli/client/settings.py +1 -0
  53. purviewcli/client/sync_client.py +250 -0
  54. purviewcli/plugins/__init__.py +1 -0
  55. purviewcli/plugins/plugin_system.py +709 -0
  56. pvw_cli-1.2.8.dist-info/METADATA +1618 -0
  57. pvw_cli-1.2.8.dist-info/RECORD +60 -0
  58. pvw_cli-1.2.8.dist-info/WHEEL +5 -0
  59. pvw_cli-1.2.8.dist-info/entry_points.txt +3 -0
  60. 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