truthound-dashboard 1.3.0__py3-none-any.whl → 1.4.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.
- 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.0.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -18
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BCA8H1hO.js +0 -574
- truthound_dashboard/static/assets/index-BNsSQ2fN.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CsJWCRx9.js +0 -1
- truthound_dashboard-1.3.0.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Notification deduplication system.
|
|
2
|
+
|
|
3
|
+
This module provides a flexible deduplication system to prevent
|
|
4
|
+
sending duplicate notifications within configurable time windows.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- 4 window strategies (sliding, tumbling, session, adaptive)
|
|
8
|
+
- 6 deduplication policies (none, basic, severity, issue_based, strict, custom)
|
|
9
|
+
- Pluggable storage backends (in-memory, SQLite, Redis, Redis Streams)
|
|
10
|
+
- Fingerprint-based duplicate detection
|
|
11
|
+
- Factory function for easy store creation
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
from truthound_dashboard.core.notifications.deduplication import (
|
|
15
|
+
NotificationDeduplicator,
|
|
16
|
+
InMemoryDeduplicationStore,
|
|
17
|
+
DeduplicationPolicy,
|
|
18
|
+
TimeWindow,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Create deduplicator
|
|
22
|
+
deduplicator = NotificationDeduplicator(
|
|
23
|
+
store=InMemoryDeduplicationStore(),
|
|
24
|
+
default_window=TimeWindow(seconds=300),
|
|
25
|
+
policy=DeduplicationPolicy.SEVERITY,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Generate fingerprint and check
|
|
29
|
+
fingerprint = deduplicator.generate_fingerprint(
|
|
30
|
+
checkpoint_name="daily_check",
|
|
31
|
+
action_type="slack",
|
|
32
|
+
severity="high",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if not deduplicator.is_duplicate(fingerprint):
|
|
36
|
+
await send_notification()
|
|
37
|
+
deduplicator.mark_sent(fingerprint)
|
|
38
|
+
|
|
39
|
+
Redis Store Example:
|
|
40
|
+
from truthound_dashboard.core.notifications.deduplication import (
|
|
41
|
+
RedisDeduplicationStore,
|
|
42
|
+
REDIS_AVAILABLE,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if REDIS_AVAILABLE:
|
|
46
|
+
store = RedisDeduplicationStore(
|
|
47
|
+
redis_url="redis://localhost:6379/0",
|
|
48
|
+
key_prefix="myapp:dedup:",
|
|
49
|
+
default_ttl=3600,
|
|
50
|
+
)
|
|
51
|
+
# Use store with NotificationDeduplicator
|
|
52
|
+
|
|
53
|
+
Redis Streams Store Example (Production):
|
|
54
|
+
from truthound_dashboard.core.notifications.deduplication import (
|
|
55
|
+
RedisStreamsDeduplicationStore,
|
|
56
|
+
create_deduplication_store,
|
|
57
|
+
DeduplicationStoreType,
|
|
58
|
+
REDIS_AVAILABLE,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Using factory function (recommended)
|
|
62
|
+
store = create_deduplication_store("redis_streams")
|
|
63
|
+
|
|
64
|
+
# Or direct instantiation with full configuration
|
|
65
|
+
if REDIS_AVAILABLE:
|
|
66
|
+
store = RedisStreamsDeduplicationStore(
|
|
67
|
+
redis_url="redis://myredis:6379/1",
|
|
68
|
+
default_ttl=7200,
|
|
69
|
+
max_connections=20,
|
|
70
|
+
enable_fallback=True, # Falls back to InMemory on Redis failure
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Health check
|
|
74
|
+
health = await store.health_check_async()
|
|
75
|
+
print(f"Healthy: {health['healthy']}, Mode: {health.get('mode')}")
|
|
76
|
+
|
|
77
|
+
# Get metrics
|
|
78
|
+
metrics = store.get_metrics()
|
|
79
|
+
print(f"Hit rate: {metrics['hit_rate']}%")
|
|
80
|
+
|
|
81
|
+
Factory Function Example:
|
|
82
|
+
from truthound_dashboard.core.notifications.deduplication import (
|
|
83
|
+
create_deduplication_store,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Auto-detect from environment variables
|
|
87
|
+
store = create_deduplication_store()
|
|
88
|
+
|
|
89
|
+
# Explicit type with configuration
|
|
90
|
+
store = create_deduplication_store(
|
|
91
|
+
"redis_streams",
|
|
92
|
+
default_ttl=3600,
|
|
93
|
+
enable_fallback=True,
|
|
94
|
+
)
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
from .policies import DeduplicationPolicy, FingerprintGenerator
|
|
98
|
+
from .service import NotificationDeduplicator, TimeWindow
|
|
99
|
+
from .stores import (
|
|
100
|
+
REDIS_AVAILABLE,
|
|
101
|
+
BaseDeduplicationStore,
|
|
102
|
+
DeduplicationMetrics,
|
|
103
|
+
DeduplicationStoreType,
|
|
104
|
+
InMemoryDeduplicationStore,
|
|
105
|
+
RedisDeduplicationStore,
|
|
106
|
+
RedisStreamsDeduplicationStore,
|
|
107
|
+
SQLiteDeduplicationStore,
|
|
108
|
+
create_deduplication_store,
|
|
109
|
+
)
|
|
110
|
+
from .strategies import (
|
|
111
|
+
AdaptiveWindowStrategy,
|
|
112
|
+
BaseWindowStrategy,
|
|
113
|
+
SessionWindowStrategy,
|
|
114
|
+
SlidingWindowStrategy,
|
|
115
|
+
StrategyRegistry,
|
|
116
|
+
TumblingWindowStrategy,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
__all__ = [
|
|
120
|
+
# Stores
|
|
121
|
+
"BaseDeduplicationStore",
|
|
122
|
+
"InMemoryDeduplicationStore",
|
|
123
|
+
"SQLiteDeduplicationStore",
|
|
124
|
+
"RedisDeduplicationStore",
|
|
125
|
+
"RedisStreamsDeduplicationStore",
|
|
126
|
+
"DeduplicationMetrics",
|
|
127
|
+
"DeduplicationStoreType",
|
|
128
|
+
"create_deduplication_store",
|
|
129
|
+
"REDIS_AVAILABLE",
|
|
130
|
+
# Strategies
|
|
131
|
+
"BaseWindowStrategy",
|
|
132
|
+
"SlidingWindowStrategy",
|
|
133
|
+
"TumblingWindowStrategy",
|
|
134
|
+
"SessionWindowStrategy",
|
|
135
|
+
"AdaptiveWindowStrategy",
|
|
136
|
+
"StrategyRegistry",
|
|
137
|
+
# Policies
|
|
138
|
+
"DeduplicationPolicy",
|
|
139
|
+
"FingerprintGenerator",
|
|
140
|
+
# Service
|
|
141
|
+
"NotificationDeduplicator",
|
|
142
|
+
"TimeWindow",
|
|
143
|
+
]
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Deduplication policies and fingerprint generation.
|
|
2
|
+
|
|
3
|
+
This module provides policies that determine what fields are
|
|
4
|
+
included in the deduplication fingerprint.
|
|
5
|
+
|
|
6
|
+
Policies:
|
|
7
|
+
- NONE: No deduplication (testing)
|
|
8
|
+
- BASIC: checkpoint_name + action_type
|
|
9
|
+
- SEVERITY: + severity level
|
|
10
|
+
- ISSUE_BASED: + issue hash
|
|
11
|
+
- STRICT: + timestamp bucket
|
|
12
|
+
- CUSTOM: User-defined fields
|
|
13
|
+
|
|
14
|
+
Different policies trade off between:
|
|
15
|
+
- Granularity: How specific the deduplication is
|
|
16
|
+
- Suppression: How many notifications are blocked
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DeduplicationPolicy(str, Enum):
|
|
29
|
+
"""Deduplication policy types.
|
|
30
|
+
|
|
31
|
+
Defines what fields are included in the fingerprint.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# No deduplication - every event is unique
|
|
35
|
+
NONE = "none"
|
|
36
|
+
|
|
37
|
+
# Basic: checkpoint name + action type
|
|
38
|
+
BASIC = "basic"
|
|
39
|
+
|
|
40
|
+
# Severity: basic + severity level
|
|
41
|
+
SEVERITY = "severity"
|
|
42
|
+
|
|
43
|
+
# Issue-based: basic + issue content hash
|
|
44
|
+
ISSUE_BASED = "issue_based"
|
|
45
|
+
|
|
46
|
+
# Strict: basic + timestamp bucket
|
|
47
|
+
STRICT = "strict"
|
|
48
|
+
|
|
49
|
+
# Custom: user-defined fields
|
|
50
|
+
CUSTOM = "custom"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class FingerprintConfig:
|
|
55
|
+
"""Configuration for fingerprint generation.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
include_checkpoint: Include checkpoint/source name.
|
|
59
|
+
include_action_type: Include action/channel type.
|
|
60
|
+
include_severity: Include severity level.
|
|
61
|
+
include_issue_hash: Include hash of issue details.
|
|
62
|
+
include_timestamp_bucket: Include timestamp bucket.
|
|
63
|
+
timestamp_bucket_seconds: Size of timestamp buckets.
|
|
64
|
+
custom_fields: Additional fields to include.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
include_checkpoint: bool = True
|
|
68
|
+
include_action_type: bool = True
|
|
69
|
+
include_severity: bool = False
|
|
70
|
+
include_issue_hash: bool = False
|
|
71
|
+
include_timestamp_bucket: bool = False
|
|
72
|
+
timestamp_bucket_seconds: int = 300
|
|
73
|
+
custom_fields: list[str] = field(default_factory=list)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_policy(cls, policy: DeduplicationPolicy) -> "FingerprintConfig":
|
|
77
|
+
"""Create config from policy preset.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
policy: Deduplication policy.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Configured FingerprintConfig.
|
|
84
|
+
"""
|
|
85
|
+
if policy == DeduplicationPolicy.NONE:
|
|
86
|
+
return cls(
|
|
87
|
+
include_checkpoint=False,
|
|
88
|
+
include_action_type=False,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
elif policy == DeduplicationPolicy.BASIC:
|
|
92
|
+
return cls(
|
|
93
|
+
include_checkpoint=True,
|
|
94
|
+
include_action_type=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
elif policy == DeduplicationPolicy.SEVERITY:
|
|
98
|
+
return cls(
|
|
99
|
+
include_checkpoint=True,
|
|
100
|
+
include_action_type=True,
|
|
101
|
+
include_severity=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
elif policy == DeduplicationPolicy.ISSUE_BASED:
|
|
105
|
+
return cls(
|
|
106
|
+
include_checkpoint=True,
|
|
107
|
+
include_action_type=True,
|
|
108
|
+
include_issue_hash=True,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
elif policy == DeduplicationPolicy.STRICT:
|
|
112
|
+
return cls(
|
|
113
|
+
include_checkpoint=True,
|
|
114
|
+
include_action_type=True,
|
|
115
|
+
include_severity=True,
|
|
116
|
+
include_timestamp_bucket=True,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
elif policy == DeduplicationPolicy.CUSTOM:
|
|
120
|
+
# Custom requires explicit configuration
|
|
121
|
+
return cls()
|
|
122
|
+
|
|
123
|
+
return cls()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class FingerprintGenerator:
|
|
127
|
+
"""Generates deduplication fingerprints.
|
|
128
|
+
|
|
129
|
+
The fingerprint uniquely identifies a notification for
|
|
130
|
+
deduplication purposes. Two events with the same fingerprint
|
|
131
|
+
within the deduplication window will be considered duplicates.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
generator = FingerprintGenerator(
|
|
135
|
+
policy=DeduplicationPolicy.SEVERITY
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
fingerprint = generator.generate(
|
|
139
|
+
checkpoint_name="daily_check",
|
|
140
|
+
action_type="slack",
|
|
141
|
+
severity="high",
|
|
142
|
+
)
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
policy: DeduplicationPolicy = DeduplicationPolicy.BASIC,
|
|
148
|
+
config: FingerprintConfig | None = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Initialize fingerprint generator.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
policy: Deduplication policy to use.
|
|
154
|
+
config: Optional explicit configuration (overrides policy).
|
|
155
|
+
"""
|
|
156
|
+
self.policy = policy
|
|
157
|
+
self.config = config or FingerprintConfig.from_policy(policy)
|
|
158
|
+
|
|
159
|
+
def generate(
|
|
160
|
+
self,
|
|
161
|
+
checkpoint_name: str | None = None,
|
|
162
|
+
action_type: str | None = None,
|
|
163
|
+
severity: str | None = None,
|
|
164
|
+
issues: list[dict[str, Any]] | None = None,
|
|
165
|
+
timestamp: datetime | None = None,
|
|
166
|
+
**custom_fields: Any,
|
|
167
|
+
) -> str:
|
|
168
|
+
"""Generate a fingerprint from event data.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
checkpoint_name: Name of checkpoint/source.
|
|
172
|
+
action_type: Type of notification action.
|
|
173
|
+
severity: Severity level.
|
|
174
|
+
issues: List of issues for hashing.
|
|
175
|
+
timestamp: Event timestamp.
|
|
176
|
+
**custom_fields: Additional fields for custom policy.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Generated fingerprint string.
|
|
180
|
+
"""
|
|
181
|
+
if self.policy == DeduplicationPolicy.NONE:
|
|
182
|
+
# Unique fingerprint for each event
|
|
183
|
+
return self._random_fingerprint()
|
|
184
|
+
|
|
185
|
+
parts: list[str] = []
|
|
186
|
+
|
|
187
|
+
# Add configured fields
|
|
188
|
+
if self.config.include_checkpoint and checkpoint_name:
|
|
189
|
+
parts.append(f"cp:{checkpoint_name}")
|
|
190
|
+
|
|
191
|
+
if self.config.include_action_type and action_type:
|
|
192
|
+
parts.append(f"act:{action_type}")
|
|
193
|
+
|
|
194
|
+
if self.config.include_severity and severity:
|
|
195
|
+
parts.append(f"sev:{severity}")
|
|
196
|
+
|
|
197
|
+
if self.config.include_issue_hash and issues:
|
|
198
|
+
issue_hash = self._hash_issues(issues)
|
|
199
|
+
parts.append(f"ish:{issue_hash}")
|
|
200
|
+
|
|
201
|
+
if self.config.include_timestamp_bucket:
|
|
202
|
+
bucket = self._get_timestamp_bucket(timestamp)
|
|
203
|
+
parts.append(f"ts:{bucket}")
|
|
204
|
+
|
|
205
|
+
# Add custom fields
|
|
206
|
+
for field_name in self.config.custom_fields:
|
|
207
|
+
if field_name in custom_fields:
|
|
208
|
+
value = custom_fields[field_name]
|
|
209
|
+
parts.append(f"{field_name}:{value}")
|
|
210
|
+
|
|
211
|
+
# Also add any extra custom fields passed
|
|
212
|
+
for field_name, value in custom_fields.items():
|
|
213
|
+
if field_name not in self.config.custom_fields:
|
|
214
|
+
parts.append(f"{field_name}:{value}")
|
|
215
|
+
|
|
216
|
+
if not parts:
|
|
217
|
+
# Fallback to random if no parts
|
|
218
|
+
return self._random_fingerprint()
|
|
219
|
+
|
|
220
|
+
# Join parts and hash
|
|
221
|
+
combined = "|".join(sorted(parts))
|
|
222
|
+
return hashlib.sha256(combined.encode()).hexdigest()[:32]
|
|
223
|
+
|
|
224
|
+
def _hash_issues(self, issues: list[dict[str, Any]]) -> str:
|
|
225
|
+
"""Generate hash from issue details."""
|
|
226
|
+
import json
|
|
227
|
+
|
|
228
|
+
# Sort issues for consistent hashing
|
|
229
|
+
try:
|
|
230
|
+
normalized = json.dumps(issues, sort_keys=True)
|
|
231
|
+
except (TypeError, ValueError):
|
|
232
|
+
normalized = str(issues)
|
|
233
|
+
|
|
234
|
+
return hashlib.sha256(normalized.encode()).hexdigest()[:16]
|
|
235
|
+
|
|
236
|
+
def _get_timestamp_bucket(self, timestamp: datetime | None) -> int:
|
|
237
|
+
"""Get timestamp bucket number."""
|
|
238
|
+
if timestamp is None:
|
|
239
|
+
timestamp = datetime.utcnow()
|
|
240
|
+
|
|
241
|
+
ts = timestamp.timestamp()
|
|
242
|
+
bucket_size = self.config.timestamp_bucket_seconds
|
|
243
|
+
return int(ts // bucket_size)
|
|
244
|
+
|
|
245
|
+
def _random_fingerprint(self) -> str:
|
|
246
|
+
"""Generate a random unique fingerprint."""
|
|
247
|
+
import secrets
|
|
248
|
+
|
|
249
|
+
return secrets.token_hex(16)
|
|
250
|
+
|
|
251
|
+
def with_policy(self, policy: DeduplicationPolicy) -> "FingerprintGenerator":
|
|
252
|
+
"""Create new generator with different policy.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
policy: New policy to use.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
New FingerprintGenerator instance.
|
|
259
|
+
"""
|
|
260
|
+
return FingerprintGenerator(policy=policy)
|
|
261
|
+
|
|
262
|
+
def with_config(self, config: FingerprintConfig) -> "FingerprintGenerator":
|
|
263
|
+
"""Create new generator with custom config.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
config: Custom configuration.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
New FingerprintGenerator instance.
|
|
270
|
+
"""
|
|
271
|
+
return FingerprintGenerator(
|
|
272
|
+
policy=DeduplicationPolicy.CUSTOM,
|
|
273
|
+
config=config,
|
|
274
|
+
)
|