truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.1__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.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
- truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"""Specialized metrics collectors for notification subsystems.
|
|
2
|
+
|
|
3
|
+
This module provides thread-safe metrics collectors for:
|
|
4
|
+
- DeduplicationMetrics: Track deduplication rates and active fingerprints
|
|
5
|
+
- ThrottlingMetrics: Track throttling rates and window counts
|
|
6
|
+
- EscalationMetrics: Track incidents by state and resolution times
|
|
7
|
+
|
|
8
|
+
All collectors use asyncio.Lock for thread-safe operations.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
# Create metrics collector
|
|
12
|
+
dedup_metrics = DeduplicationMetrics()
|
|
13
|
+
|
|
14
|
+
# Record events
|
|
15
|
+
await dedup_metrics.record_received()
|
|
16
|
+
if is_duplicate:
|
|
17
|
+
await dedup_metrics.record_deduplicated()
|
|
18
|
+
else:
|
|
19
|
+
await dedup_metrics.record_passed()
|
|
20
|
+
|
|
21
|
+
# Get statistics
|
|
22
|
+
stats = await dedup_metrics.get_stats()
|
|
23
|
+
print(f"Deduplication rate: {stats['dedup_rate']:.2%}")
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DeduplicationStats:
|
|
36
|
+
"""Statistics from deduplication metrics.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
total_received: Total notifications received.
|
|
40
|
+
total_deduplicated: Total notifications deduplicated (skipped).
|
|
41
|
+
total_passed: Total notifications passed through.
|
|
42
|
+
dedup_rate: Deduplication rate (0.0 to 1.0).
|
|
43
|
+
active_fingerprints: Number of active fingerprints being tracked.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
total_received: int
|
|
47
|
+
total_deduplicated: int
|
|
48
|
+
total_passed: int
|
|
49
|
+
dedup_rate: float
|
|
50
|
+
active_fingerprints: int
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, Any]:
|
|
53
|
+
"""Serialize to dictionary."""
|
|
54
|
+
return {
|
|
55
|
+
"total_received": self.total_received,
|
|
56
|
+
"total_deduplicated": self.total_deduplicated,
|
|
57
|
+
"total_passed": self.total_passed,
|
|
58
|
+
"dedup_rate": self.dedup_rate,
|
|
59
|
+
"active_fingerprints": self.active_fingerprints,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DeduplicationMetrics:
|
|
64
|
+
"""Thread-safe metrics collector for deduplication.
|
|
65
|
+
|
|
66
|
+
Tracks total notifications received, deduplicated, and passed through
|
|
67
|
+
the deduplication service.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
metrics = DeduplicationMetrics()
|
|
71
|
+
|
|
72
|
+
# Record a received notification
|
|
73
|
+
await metrics.record_received()
|
|
74
|
+
|
|
75
|
+
# Record deduplication decision
|
|
76
|
+
if is_duplicate:
|
|
77
|
+
await metrics.record_deduplicated()
|
|
78
|
+
else:
|
|
79
|
+
await metrics.record_passed()
|
|
80
|
+
|
|
81
|
+
# Get current stats
|
|
82
|
+
stats = await metrics.get_stats()
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self) -> None:
|
|
86
|
+
"""Initialize deduplication metrics."""
|
|
87
|
+
self._lock = asyncio.Lock()
|
|
88
|
+
self._total_received: int = 0
|
|
89
|
+
self._total_deduplicated: int = 0
|
|
90
|
+
self._total_passed: int = 0
|
|
91
|
+
self._active_fingerprints: set[str] = set()
|
|
92
|
+
|
|
93
|
+
async def record_received(self, fingerprint: str | None = None) -> None:
|
|
94
|
+
"""Record a notification received for deduplication check.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
fingerprint: Optional fingerprint to track as active.
|
|
98
|
+
"""
|
|
99
|
+
async with self._lock:
|
|
100
|
+
self._total_received += 1
|
|
101
|
+
if fingerprint:
|
|
102
|
+
self._active_fingerprints.add(fingerprint)
|
|
103
|
+
|
|
104
|
+
async def record_deduplicated(self, fingerprint: str | None = None) -> None:
|
|
105
|
+
"""Record a notification that was deduplicated (skipped).
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
fingerprint: Optional fingerprint that was deduplicated.
|
|
109
|
+
"""
|
|
110
|
+
async with self._lock:
|
|
111
|
+
self._total_deduplicated += 1
|
|
112
|
+
|
|
113
|
+
async def record_passed(self, fingerprint: str | None = None) -> None:
|
|
114
|
+
"""Record a notification that passed deduplication.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
fingerprint: Optional fingerprint that passed.
|
|
118
|
+
"""
|
|
119
|
+
async with self._lock:
|
|
120
|
+
self._total_passed += 1
|
|
121
|
+
|
|
122
|
+
async def remove_fingerprint(self, fingerprint: str) -> None:
|
|
123
|
+
"""Remove a fingerprint from active tracking.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
fingerprint: Fingerprint to remove.
|
|
127
|
+
"""
|
|
128
|
+
async with self._lock:
|
|
129
|
+
self._active_fingerprints.discard(fingerprint)
|
|
130
|
+
|
|
131
|
+
async def clear_fingerprints(self) -> None:
|
|
132
|
+
"""Clear all active fingerprints."""
|
|
133
|
+
async with self._lock:
|
|
134
|
+
self._active_fingerprints.clear()
|
|
135
|
+
|
|
136
|
+
async def get_stats(self) -> DeduplicationStats:
|
|
137
|
+
"""Get current deduplication statistics.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
DeduplicationStats with current metrics.
|
|
141
|
+
"""
|
|
142
|
+
async with self._lock:
|
|
143
|
+
total_received = self._total_received
|
|
144
|
+
total_deduplicated = self._total_deduplicated
|
|
145
|
+
total_passed = self._total_passed
|
|
146
|
+
active_fingerprints = len(self._active_fingerprints)
|
|
147
|
+
|
|
148
|
+
# Calculate dedup rate
|
|
149
|
+
dedup_rate = 0.0
|
|
150
|
+
if total_received > 0:
|
|
151
|
+
dedup_rate = total_deduplicated / total_received
|
|
152
|
+
|
|
153
|
+
return DeduplicationStats(
|
|
154
|
+
total_received=total_received,
|
|
155
|
+
total_deduplicated=total_deduplicated,
|
|
156
|
+
total_passed=total_passed,
|
|
157
|
+
dedup_rate=dedup_rate,
|
|
158
|
+
active_fingerprints=active_fingerprints,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def reset(self) -> None:
|
|
162
|
+
"""Reset all counters and fingerprints."""
|
|
163
|
+
async with self._lock:
|
|
164
|
+
self._total_received = 0
|
|
165
|
+
self._total_deduplicated = 0
|
|
166
|
+
self._total_passed = 0
|
|
167
|
+
self._active_fingerprints.clear()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class ThrottlingStats:
|
|
172
|
+
"""Statistics from throttling metrics.
|
|
173
|
+
|
|
174
|
+
Attributes:
|
|
175
|
+
total_received: Total requests received.
|
|
176
|
+
total_throttled: Total requests throttled (rejected).
|
|
177
|
+
total_passed: Total requests passed through.
|
|
178
|
+
throttle_rate: Throttle rate (0.0 to 1.0).
|
|
179
|
+
current_window_count: Number of requests in current window.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
total_received: int
|
|
183
|
+
total_throttled: int
|
|
184
|
+
total_passed: int
|
|
185
|
+
throttle_rate: float
|
|
186
|
+
current_window_count: int
|
|
187
|
+
|
|
188
|
+
def to_dict(self) -> dict[str, Any]:
|
|
189
|
+
"""Serialize to dictionary."""
|
|
190
|
+
return {
|
|
191
|
+
"total_received": self.total_received,
|
|
192
|
+
"total_throttled": self.total_throttled,
|
|
193
|
+
"total_passed": self.total_passed,
|
|
194
|
+
"throttle_rate": self.throttle_rate,
|
|
195
|
+
"current_window_count": self.current_window_count,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclass
|
|
200
|
+
class WindowCount:
|
|
201
|
+
"""Tracks counts within a time window.
|
|
202
|
+
|
|
203
|
+
Attributes:
|
|
204
|
+
window_start: Start timestamp of the window.
|
|
205
|
+
count: Number of requests in this window.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
window_start: datetime
|
|
209
|
+
count: int = 0
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class ThrottlingMetrics:
|
|
213
|
+
"""Thread-safe metrics collector for throttling.
|
|
214
|
+
|
|
215
|
+
Tracks total requests received, throttled, and passed through
|
|
216
|
+
the throttling service. Also tracks current window counts.
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
metrics = ThrottlingMetrics(window_seconds=60)
|
|
220
|
+
|
|
221
|
+
# Record a received request
|
|
222
|
+
await metrics.record_received()
|
|
223
|
+
|
|
224
|
+
# Record throttling decision
|
|
225
|
+
if is_throttled:
|
|
226
|
+
await metrics.record_throttled()
|
|
227
|
+
else:
|
|
228
|
+
await metrics.record_passed()
|
|
229
|
+
|
|
230
|
+
# Get current stats
|
|
231
|
+
stats = await metrics.get_stats()
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
def __init__(self, window_seconds: int = 60) -> None:
|
|
235
|
+
"""Initialize throttling metrics.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
window_seconds: Window duration for current_window_count tracking.
|
|
239
|
+
"""
|
|
240
|
+
self._lock = asyncio.Lock()
|
|
241
|
+
self._total_received: int = 0
|
|
242
|
+
self._total_throttled: int = 0
|
|
243
|
+
self._total_passed: int = 0
|
|
244
|
+
self._window_seconds = window_seconds
|
|
245
|
+
self._current_window: WindowCount | None = None
|
|
246
|
+
|
|
247
|
+
def _get_window_start(self, now: datetime) -> datetime:
|
|
248
|
+
"""Get the start of the current window.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
now: Current timestamp.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Window start timestamp.
|
|
255
|
+
"""
|
|
256
|
+
# Align to window boundaries
|
|
257
|
+
timestamp = now.timestamp()
|
|
258
|
+
window_start_ts = int(timestamp // self._window_seconds) * self._window_seconds
|
|
259
|
+
return datetime.fromtimestamp(window_start_ts)
|
|
260
|
+
|
|
261
|
+
async def record_received(self) -> None:
|
|
262
|
+
"""Record a request received for throttle check."""
|
|
263
|
+
async with self._lock:
|
|
264
|
+
self._total_received += 1
|
|
265
|
+
self._increment_window_count()
|
|
266
|
+
|
|
267
|
+
async def record_throttled(self) -> None:
|
|
268
|
+
"""Record a request that was throttled (rejected)."""
|
|
269
|
+
async with self._lock:
|
|
270
|
+
self._total_throttled += 1
|
|
271
|
+
|
|
272
|
+
async def record_passed(self) -> None:
|
|
273
|
+
"""Record a request that passed throttling."""
|
|
274
|
+
async with self._lock:
|
|
275
|
+
self._total_passed += 1
|
|
276
|
+
|
|
277
|
+
def _increment_window_count(self) -> None:
|
|
278
|
+
"""Increment the current window count (must be called with lock held)."""
|
|
279
|
+
now = datetime.utcnow()
|
|
280
|
+
window_start = self._get_window_start(now)
|
|
281
|
+
|
|
282
|
+
if self._current_window is None or self._current_window.window_start != window_start:
|
|
283
|
+
# Start new window
|
|
284
|
+
self._current_window = WindowCount(window_start=window_start, count=1)
|
|
285
|
+
else:
|
|
286
|
+
# Increment existing window
|
|
287
|
+
self._current_window.count += 1
|
|
288
|
+
|
|
289
|
+
async def get_stats(self) -> ThrottlingStats:
|
|
290
|
+
"""Get current throttling statistics.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
ThrottlingStats with current metrics.
|
|
294
|
+
"""
|
|
295
|
+
async with self._lock:
|
|
296
|
+
total_received = self._total_received
|
|
297
|
+
total_throttled = self._total_throttled
|
|
298
|
+
total_passed = self._total_passed
|
|
299
|
+
|
|
300
|
+
# Get current window count
|
|
301
|
+
now = datetime.utcnow()
|
|
302
|
+
window_start = self._get_window_start(now)
|
|
303
|
+
current_window_count = 0
|
|
304
|
+
if (
|
|
305
|
+
self._current_window is not None
|
|
306
|
+
and self._current_window.window_start == window_start
|
|
307
|
+
):
|
|
308
|
+
current_window_count = self._current_window.count
|
|
309
|
+
|
|
310
|
+
# Calculate throttle rate
|
|
311
|
+
throttle_rate = 0.0
|
|
312
|
+
if total_received > 0:
|
|
313
|
+
throttle_rate = total_throttled / total_received
|
|
314
|
+
|
|
315
|
+
return ThrottlingStats(
|
|
316
|
+
total_received=total_received,
|
|
317
|
+
total_throttled=total_throttled,
|
|
318
|
+
total_passed=total_passed,
|
|
319
|
+
throttle_rate=throttle_rate,
|
|
320
|
+
current_window_count=current_window_count,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
async def reset(self) -> None:
|
|
324
|
+
"""Reset all counters."""
|
|
325
|
+
async with self._lock:
|
|
326
|
+
self._total_received = 0
|
|
327
|
+
self._total_throttled = 0
|
|
328
|
+
self._total_passed = 0
|
|
329
|
+
self._current_window = None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@dataclass
|
|
333
|
+
class EscalationStats:
|
|
334
|
+
"""Statistics from escalation metrics.
|
|
335
|
+
|
|
336
|
+
Attributes:
|
|
337
|
+
total_incidents: Total incidents tracked.
|
|
338
|
+
by_state: Count of incidents by state.
|
|
339
|
+
active_count: Number of active (non-resolved) incidents.
|
|
340
|
+
avg_resolution_time: Average resolution time in seconds.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
total_incidents: int
|
|
344
|
+
by_state: dict[str, int]
|
|
345
|
+
active_count: int
|
|
346
|
+
avg_resolution_time: float
|
|
347
|
+
|
|
348
|
+
def to_dict(self) -> dict[str, Any]:
|
|
349
|
+
"""Serialize to dictionary."""
|
|
350
|
+
return {
|
|
351
|
+
"total_incidents": self.total_incidents,
|
|
352
|
+
"by_state": self.by_state,
|
|
353
|
+
"active_count": self.active_count,
|
|
354
|
+
"avg_resolution_time": self.avg_resolution_time,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@dataclass
|
|
359
|
+
class IncidentRecord:
|
|
360
|
+
"""Record of an incident for metrics tracking.
|
|
361
|
+
|
|
362
|
+
Attributes:
|
|
363
|
+
incident_id: Unique incident identifier.
|
|
364
|
+
state: Current incident state.
|
|
365
|
+
created_at: When incident was created.
|
|
366
|
+
resolved_at: When incident was resolved (if applicable).
|
|
367
|
+
"""
|
|
368
|
+
|
|
369
|
+
incident_id: str
|
|
370
|
+
state: str
|
|
371
|
+
created_at: datetime
|
|
372
|
+
resolved_at: datetime | None = None
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class EscalationMetrics:
|
|
376
|
+
"""Thread-safe metrics collector for escalation.
|
|
377
|
+
|
|
378
|
+
Tracks incidents by state and calculates resolution times.
|
|
379
|
+
|
|
380
|
+
Example:
|
|
381
|
+
metrics = EscalationMetrics()
|
|
382
|
+
|
|
383
|
+
# Record incident creation
|
|
384
|
+
await metrics.record_incident_created("incident-1")
|
|
385
|
+
|
|
386
|
+
# Update incident state
|
|
387
|
+
await metrics.record_state_change("incident-1", "triggered")
|
|
388
|
+
await metrics.record_state_change("incident-1", "acknowledged")
|
|
389
|
+
|
|
390
|
+
# Record resolution
|
|
391
|
+
await metrics.record_incident_resolved("incident-1")
|
|
392
|
+
|
|
393
|
+
# Get current stats
|
|
394
|
+
stats = await metrics.get_stats()
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
def __init__(self) -> None:
|
|
398
|
+
"""Initialize escalation metrics."""
|
|
399
|
+
self._lock = asyncio.Lock()
|
|
400
|
+
self._incidents: dict[str, IncidentRecord] = {}
|
|
401
|
+
self._resolution_times: list[float] = []
|
|
402
|
+
|
|
403
|
+
async def record_incident_created(
|
|
404
|
+
self,
|
|
405
|
+
incident_id: str,
|
|
406
|
+
initial_state: str = "pending",
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Record a new incident creation.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
incident_id: Unique incident identifier.
|
|
412
|
+
initial_state: Initial state of the incident.
|
|
413
|
+
"""
|
|
414
|
+
async with self._lock:
|
|
415
|
+
self._incidents[incident_id] = IncidentRecord(
|
|
416
|
+
incident_id=incident_id,
|
|
417
|
+
state=initial_state,
|
|
418
|
+
created_at=datetime.utcnow(),
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
async def record_state_change(
|
|
422
|
+
self,
|
|
423
|
+
incident_id: str,
|
|
424
|
+
new_state: str,
|
|
425
|
+
) -> None:
|
|
426
|
+
"""Record an incident state change.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
incident_id: Incident identifier.
|
|
430
|
+
new_state: New state of the incident.
|
|
431
|
+
"""
|
|
432
|
+
async with self._lock:
|
|
433
|
+
if incident_id in self._incidents:
|
|
434
|
+
self._incidents[incident_id].state = new_state
|
|
435
|
+
|
|
436
|
+
async def record_incident_resolved(
|
|
437
|
+
self,
|
|
438
|
+
incident_id: str,
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Record an incident resolution.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
incident_id: Incident identifier.
|
|
444
|
+
"""
|
|
445
|
+
async with self._lock:
|
|
446
|
+
if incident_id in self._incidents:
|
|
447
|
+
incident = self._incidents[incident_id]
|
|
448
|
+
incident.state = "resolved"
|
|
449
|
+
incident.resolved_at = datetime.utcnow()
|
|
450
|
+
|
|
451
|
+
# Calculate resolution time
|
|
452
|
+
resolution_time = (
|
|
453
|
+
incident.resolved_at - incident.created_at
|
|
454
|
+
).total_seconds()
|
|
455
|
+
self._resolution_times.append(resolution_time)
|
|
456
|
+
|
|
457
|
+
async def remove_incident(self, incident_id: str) -> None:
|
|
458
|
+
"""Remove an incident from tracking.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
incident_id: Incident identifier.
|
|
462
|
+
"""
|
|
463
|
+
async with self._lock:
|
|
464
|
+
self._incidents.pop(incident_id, None)
|
|
465
|
+
|
|
466
|
+
async def get_stats(self) -> EscalationStats:
|
|
467
|
+
"""Get current escalation statistics.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
EscalationStats with current metrics.
|
|
471
|
+
"""
|
|
472
|
+
async with self._lock:
|
|
473
|
+
total_incidents = len(self._incidents)
|
|
474
|
+
|
|
475
|
+
# Count by state
|
|
476
|
+
by_state: dict[str, int] = {}
|
|
477
|
+
active_count = 0
|
|
478
|
+
for incident in self._incidents.values():
|
|
479
|
+
state = incident.state
|
|
480
|
+
by_state[state] = by_state.get(state, 0) + 1
|
|
481
|
+
if state != "resolved":
|
|
482
|
+
active_count += 1
|
|
483
|
+
|
|
484
|
+
# Calculate average resolution time
|
|
485
|
+
avg_resolution_time = 0.0
|
|
486
|
+
if self._resolution_times:
|
|
487
|
+
avg_resolution_time = sum(self._resolution_times) / len(
|
|
488
|
+
self._resolution_times
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
return EscalationStats(
|
|
492
|
+
total_incidents=total_incidents,
|
|
493
|
+
by_state=by_state,
|
|
494
|
+
active_count=active_count,
|
|
495
|
+
avg_resolution_time=avg_resolution_time,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
async def reset(self) -> None:
|
|
499
|
+
"""Reset all incident tracking and resolution times."""
|
|
500
|
+
async with self._lock:
|
|
501
|
+
self._incidents.clear()
|
|
502
|
+
self._resolution_times.clear()
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# Convenience alias for combined metrics
|
|
506
|
+
@dataclass
|
|
507
|
+
class NotificationMetrics:
|
|
508
|
+
"""Combined metrics from all notification subsystems.
|
|
509
|
+
|
|
510
|
+
Attributes:
|
|
511
|
+
deduplication: Deduplication statistics.
|
|
512
|
+
throttling: Throttling statistics.
|
|
513
|
+
escalation: Escalation statistics.
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
deduplication: DeduplicationStats
|
|
517
|
+
throttling: ThrottlingStats
|
|
518
|
+
escalation: EscalationStats
|
|
519
|
+
|
|
520
|
+
def to_dict(self) -> dict[str, Any]:
|
|
521
|
+
"""Serialize to dictionary."""
|
|
522
|
+
return {
|
|
523
|
+
"deduplication": self.deduplication.to_dict(),
|
|
524
|
+
"throttling": self.throttling.to_dict(),
|
|
525
|
+
"escalation": self.escalation.to_dict(),
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class MetricsCollector:
|
|
530
|
+
"""Aggregated metrics collector for notification subsystems.
|
|
531
|
+
|
|
532
|
+
Provides a single interface to collect metrics from deduplication,
|
|
533
|
+
throttling, and escalation services.
|
|
534
|
+
|
|
535
|
+
Example:
|
|
536
|
+
collector = MetricsCollector()
|
|
537
|
+
|
|
538
|
+
# Record deduplication metrics
|
|
539
|
+
await collector.deduplication.record_received()
|
|
540
|
+
await collector.deduplication.record_deduplicated()
|
|
541
|
+
|
|
542
|
+
# Record throttling metrics
|
|
543
|
+
await collector.throttling.record_received()
|
|
544
|
+
await collector.throttling.record_passed()
|
|
545
|
+
|
|
546
|
+
# Record escalation metrics
|
|
547
|
+
await collector.escalation.record_incident_created("inc-1")
|
|
548
|
+
|
|
549
|
+
# Get combined stats
|
|
550
|
+
stats = await collector.get_all_stats()
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
def __init__(self, throttling_window_seconds: int = 60) -> None:
|
|
554
|
+
"""Initialize metrics collector.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
throttling_window_seconds: Window duration for throttling metrics.
|
|
558
|
+
"""
|
|
559
|
+
self.deduplication = DeduplicationMetrics()
|
|
560
|
+
self.throttling = ThrottlingMetrics(window_seconds=throttling_window_seconds)
|
|
561
|
+
self.escalation = EscalationMetrics()
|
|
562
|
+
|
|
563
|
+
async def get_all_stats(self) -> NotificationMetrics:
|
|
564
|
+
"""Get combined statistics from all collectors.
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
NotificationMetrics with stats from all subsystems.
|
|
568
|
+
"""
|
|
569
|
+
dedup_stats = await self.deduplication.get_stats()
|
|
570
|
+
throttle_stats = await self.throttling.get_stats()
|
|
571
|
+
escalation_stats = await self.escalation.get_stats()
|
|
572
|
+
|
|
573
|
+
return NotificationMetrics(
|
|
574
|
+
deduplication=dedup_stats,
|
|
575
|
+
throttling=throttle_stats,
|
|
576
|
+
escalation=escalation_stats,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
async def reset_all(self) -> None:
|
|
580
|
+
"""Reset all metrics collectors."""
|
|
581
|
+
await self.deduplication.reset()
|
|
582
|
+
await self.throttling.reset()
|
|
583
|
+
await self.escalation.reset()
|