omnibase_infra 0.3.2__py3-none-any.whl → 0.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.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/errors/__init__.py +4 -0
- omnibase_infra/errors/error_infra.py +60 -0
- omnibase_infra/handlers/__init__.py +3 -0
- omnibase_infra/handlers/handler_slack_webhook.py +426 -0
- omnibase_infra/handlers/models/__init__.py +14 -0
- omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
- omnibase_infra/handlers/models/model_slack_alert.py +24 -0
- omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
- omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
- omnibase_infra/mixins/mixin_node_introspection.py +42 -20
- omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
- omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
- omnibase_infra/models/discovery/model_introspection_config.py +28 -1
- omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
- omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
- omnibase_infra/models/runtime/__init__.py +4 -0
- omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
- omnibase_infra/nodes/node_contract_persistence_effect/node.py +18 -1
- omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +33 -2
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
- omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
- omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
- omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
- omnibase_infra/runtime/__init__.py +7 -0
- omnibase_infra/runtime/baseline_subscriptions.py +13 -6
- omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
- omnibase_infra/runtime/contract_registration_event_router.py +5 -5
- omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
- omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
- omnibase_infra/runtime/registry_policy.py +29 -15
- omnibase_infra/runtime/request_response_wiring.py +15 -7
- omnibase_infra/runtime/service_runtime_host_process.py +149 -5
- omnibase_infra/runtime/util_version.py +5 -1
- omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
- omnibase_infra/services/contract_publisher/config.py +4 -4
- omnibase_infra/services/contract_publisher/service.py +8 -5
- omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
- omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
- omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
- omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
- omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
- omnibase_infra/utils/__init__.py +7 -0
- omnibase_infra/utils/util_db_error_context.py +292 -0
- omnibase_infra/validation/validation_exemptions.yaml +11 -0
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +2 -2
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +57 -36
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
omnibase_infra/__init__.py
CHANGED
|
@@ -10,6 +10,7 @@ Exports:
|
|
|
10
10
|
ModelInfraErrorContext: Configuration model for bundled error context
|
|
11
11
|
RuntimeHostError: Base infrastructure error class
|
|
12
12
|
ProtocolConfigurationError: Protocol configuration validation errors
|
|
13
|
+
ProtocolDependencyResolutionError: Protocol dependency resolution errors
|
|
13
14
|
SecretResolutionError: Secret/credential resolution errors
|
|
14
15
|
InfraConnectionError: Infrastructure connection errors
|
|
15
16
|
InfraTimeoutError: Infrastructure timeout errors
|
|
@@ -115,6 +116,7 @@ from omnibase_infra.errors.error_infra import (
|
|
|
115
116
|
InfraTimeoutError,
|
|
116
117
|
InfraUnavailableError,
|
|
117
118
|
ProtocolConfigurationError,
|
|
119
|
+
ProtocolDependencyResolutionError,
|
|
118
120
|
RuntimeHostError,
|
|
119
121
|
SecretResolutionError,
|
|
120
122
|
UnknownHandlerTypeError,
|
|
@@ -162,6 +164,8 @@ __all__: list[str] = [
|
|
|
162
164
|
"ModelTimeoutErrorContext",
|
|
163
165
|
"PolicyRegistryError",
|
|
164
166
|
"ProtocolConfigurationError",
|
|
167
|
+
# Protocol dependency resolution errors
|
|
168
|
+
"ProtocolDependencyResolutionError",
|
|
165
169
|
# Repository errors
|
|
166
170
|
"RepositoryContractError",
|
|
167
171
|
"RepositoryError",
|
|
@@ -597,6 +597,65 @@ class UnknownHandlerTypeError(RuntimeHostError):
|
|
|
597
597
|
)
|
|
598
598
|
|
|
599
599
|
|
|
600
|
+
class ProtocolDependencyResolutionError(RuntimeHostError):
|
|
601
|
+
"""Raised when protocol dependencies cannot be resolved from container.
|
|
602
|
+
|
|
603
|
+
Used when a node's contract.yaml declares protocol dependencies that
|
|
604
|
+
are not registered in the container's service_registry. This is a
|
|
605
|
+
fail-fast error that prevents node creation with missing dependencies.
|
|
606
|
+
|
|
607
|
+
Example:
|
|
608
|
+
>>> context = ModelInfraErrorContext(
|
|
609
|
+
... transport_type=EnumInfraTransportType.RUNTIME,
|
|
610
|
+
... operation="resolve_dependencies",
|
|
611
|
+
... target_name="NodeContractPersistenceEffect",
|
|
612
|
+
... )
|
|
613
|
+
>>> raise ProtocolDependencyResolutionError(
|
|
614
|
+
... "Missing required protocols for node",
|
|
615
|
+
... context=context,
|
|
616
|
+
... missing_protocols=["ProtocolPostgresAdapter"],
|
|
617
|
+
... node_name="node_contract_persistence_effect",
|
|
618
|
+
... )
|
|
619
|
+
|
|
620
|
+
.. versionadded:: 0.x.x
|
|
621
|
+
Part of OMN-1732 runtime dependency injection.
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
def __init__(
|
|
625
|
+
self,
|
|
626
|
+
message: str,
|
|
627
|
+
context: ModelInfraErrorContext | None = None,
|
|
628
|
+
*,
|
|
629
|
+
missing_protocols: list[str] | None = None,
|
|
630
|
+
node_name: str | None = None,
|
|
631
|
+
contract_path: str | None = None,
|
|
632
|
+
**extra_context: object,
|
|
633
|
+
) -> None:
|
|
634
|
+
"""Initialize ProtocolDependencyResolutionError.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
message: Human-readable error message
|
|
638
|
+
context: Bundled infrastructure context
|
|
639
|
+
missing_protocols: List of protocol class names that could not be resolved
|
|
640
|
+
node_name: Name of the node requiring the protocols
|
|
641
|
+
contract_path: Path to the contract.yaml file (for debugging)
|
|
642
|
+
**extra_context: Additional context information
|
|
643
|
+
"""
|
|
644
|
+
if missing_protocols is not None:
|
|
645
|
+
extra_context["missing_protocols"] = missing_protocols
|
|
646
|
+
if node_name is not None:
|
|
647
|
+
extra_context["node_name"] = node_name
|
|
648
|
+
if contract_path is not None:
|
|
649
|
+
extra_context["contract_path"] = contract_path
|
|
650
|
+
|
|
651
|
+
super().__init__(
|
|
652
|
+
message=message,
|
|
653
|
+
error_code=EnumCoreErrorCode.DEPENDENCY_ERROR,
|
|
654
|
+
context=context,
|
|
655
|
+
**extra_context,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
|
|
600
659
|
__all__: list[str] = [
|
|
601
660
|
"EnvelopeValidationError",
|
|
602
661
|
"InfraAuthenticationError",
|
|
@@ -604,6 +663,7 @@ __all__: list[str] = [
|
|
|
604
663
|
"InfraTimeoutError",
|
|
605
664
|
"InfraUnavailableError",
|
|
606
665
|
"ProtocolConfigurationError",
|
|
666
|
+
"ProtocolDependencyResolutionError",
|
|
607
667
|
"RuntimeHostError",
|
|
608
668
|
"SecretResolutionError",
|
|
609
669
|
"UnknownHandlerTypeError",
|
|
@@ -20,6 +20,7 @@ Available Handlers:
|
|
|
20
20
|
- HandlerGraph: Graph database handler (Memgraph/Neo4j via Bolt protocol)
|
|
21
21
|
- HandlerIntent: Intent storage and query handler wrapping HandlerGraph (demo wiring)
|
|
22
22
|
- HandlerQdrant: Qdrant vector database handler (MVP: create, upsert, search, delete)
|
|
23
|
+
- HandlerSlackWebhook: Slack webhook handler for infrastructure alerting
|
|
23
24
|
|
|
24
25
|
Response Models:
|
|
25
26
|
- ModelDbQueryPayload: Database query result payload
|
|
@@ -45,6 +46,7 @@ from omnibase_infra.handlers.handler_manifest_persistence import (
|
|
|
45
46
|
)
|
|
46
47
|
from omnibase_infra.handlers.handler_mcp import HandlerMCP
|
|
47
48
|
from omnibase_infra.handlers.handler_qdrant import HandlerQdrant
|
|
49
|
+
from omnibase_infra.handlers.handler_slack_webhook import HandlerSlackWebhook
|
|
48
50
|
from omnibase_infra.handlers.handler_vault import HandlerVault
|
|
49
51
|
from omnibase_infra.handlers.models import (
|
|
50
52
|
ModelConsulHandlerPayload,
|
|
@@ -71,6 +73,7 @@ __all__: list[str] = [
|
|
|
71
73
|
"HandlerManifestPersistence",
|
|
72
74
|
"HandlerMCP",
|
|
73
75
|
"HandlerQdrant",
|
|
76
|
+
"HandlerSlackWebhook",
|
|
74
77
|
"HandlerVault",
|
|
75
78
|
"ModelConsulHandlerPayload",
|
|
76
79
|
"ModelConsulHandlerResponse",
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Slack Webhook Handler - Infrastructure alerting via Slack webhooks.
|
|
4
|
+
|
|
5
|
+
This handler sends alerts to Slack channels using incoming webhooks,
|
|
6
|
+
with support for Block Kit formatting, retry with exponential backoff,
|
|
7
|
+
and rate limit handling.
|
|
8
|
+
|
|
9
|
+
Architecture:
|
|
10
|
+
This handler follows the ONEX operation handler pattern:
|
|
11
|
+
- Receives typed input (ModelSlackAlert)
|
|
12
|
+
- Executes a single responsibility (Slack webhook delivery)
|
|
13
|
+
- Returns typed output (ModelSlackAlertResult)
|
|
14
|
+
- Uses error sanitization for security
|
|
15
|
+
- Stateless and coroutine-safe for concurrent calls
|
|
16
|
+
|
|
17
|
+
Handler Responsibilities:
|
|
18
|
+
- Format alerts as Slack Block Kit messages
|
|
19
|
+
- Send webhooks with configurable retry logic
|
|
20
|
+
- Handle 429 rate limiting gracefully
|
|
21
|
+
- Sanitize errors to prevent credential exposure
|
|
22
|
+
- Track operation timing and retry counts
|
|
23
|
+
|
|
24
|
+
Configuration:
|
|
25
|
+
The webhook URL is resolved from the SLACK_WEBHOOK_URL environment
|
|
26
|
+
variable. This keeps credentials out of code and configuration files.
|
|
27
|
+
|
|
28
|
+
Coroutine Safety:
|
|
29
|
+
This handler is stateless and coroutine-safe for concurrent calls
|
|
30
|
+
with different request instances.
|
|
31
|
+
|
|
32
|
+
Related Tickets:
|
|
33
|
+
- OMN-1905: Add declarative Slack webhook handler to omnibase_infra
|
|
34
|
+
- OMN-1895: Wiring Health Monitor alerting (blocked by this)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import asyncio
|
|
40
|
+
import logging
|
|
41
|
+
import os
|
|
42
|
+
import time
|
|
43
|
+
from typing import TYPE_CHECKING
|
|
44
|
+
|
|
45
|
+
import aiohttp
|
|
46
|
+
|
|
47
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
48
|
+
from omnibase_infra.errors import (
|
|
49
|
+
InfraConnectionError,
|
|
50
|
+
InfraTimeoutError,
|
|
51
|
+
InfraUnavailableError,
|
|
52
|
+
ModelInfraErrorContext,
|
|
53
|
+
ProtocolConfigurationError,
|
|
54
|
+
)
|
|
55
|
+
from omnibase_infra.handlers.models.model_slack_alert import (
|
|
56
|
+
EnumAlertSeverity,
|
|
57
|
+
ModelSlackAlert,
|
|
58
|
+
ModelSlackAlertResult,
|
|
59
|
+
)
|
|
60
|
+
from omnibase_infra.utils import sanitize_error_message
|
|
61
|
+
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
from uuid import UUID
|
|
64
|
+
|
|
65
|
+
logger = logging.getLogger(__name__)
|
|
66
|
+
|
|
67
|
+
# Default retry configuration
|
|
68
|
+
_DEFAULT_MAX_RETRIES: int = 3
|
|
69
|
+
_DEFAULT_RETRY_BACKOFF_SECONDS: tuple[float, ...] = (1.0, 2.0, 4.0)
|
|
70
|
+
_DEFAULT_TIMEOUT_SECONDS: float = 10.0
|
|
71
|
+
|
|
72
|
+
# Slack Block Kit emoji mapping for severity levels
|
|
73
|
+
_SEVERITY_EMOJI: dict[EnumAlertSeverity, str] = {
|
|
74
|
+
EnumAlertSeverity.CRITICAL: ":red_circle:",
|
|
75
|
+
EnumAlertSeverity.ERROR: ":red_circle:",
|
|
76
|
+
EnumAlertSeverity.WARNING: ":large_yellow_circle:",
|
|
77
|
+
EnumAlertSeverity.INFO: ":large_blue_circle:",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Default titles for each severity level
|
|
81
|
+
_SEVERITY_TITLES: dict[EnumAlertSeverity, str] = {
|
|
82
|
+
EnumAlertSeverity.CRITICAL: "Critical Alert",
|
|
83
|
+
EnumAlertSeverity.ERROR: "Error Alert",
|
|
84
|
+
EnumAlertSeverity.WARNING: "Warning",
|
|
85
|
+
EnumAlertSeverity.INFO: "Info",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class HandlerSlackWebhook:
|
|
90
|
+
"""Handler for Slack webhook alert delivery.
|
|
91
|
+
|
|
92
|
+
Encapsulates all Slack-specific alerting logic for declarative
|
|
93
|
+
node compliance. Supports Block Kit formatting, retry with exponential
|
|
94
|
+
backoff, and rate limit handling.
|
|
95
|
+
|
|
96
|
+
Error Handling:
|
|
97
|
+
All errors are sanitized before inclusion in the result to prevent
|
|
98
|
+
credential exposure. The handler never raises exceptions during
|
|
99
|
+
normal operation - errors are captured in ModelSlackAlertResult.
|
|
100
|
+
|
|
101
|
+
Rate Limiting:
|
|
102
|
+
HTTP 429 responses trigger automatic retry with backoff. After
|
|
103
|
+
max retries are exhausted, the operation fails gracefully with
|
|
104
|
+
an error result rather than raising an exception.
|
|
105
|
+
|
|
106
|
+
Attributes:
|
|
107
|
+
_webhook_url: Slack webhook URL (from env or constructor)
|
|
108
|
+
_http_session: Optional shared aiohttp session
|
|
109
|
+
_max_retries: Maximum retry attempts for failed requests
|
|
110
|
+
_retry_backoff: Tuple of backoff delays in seconds
|
|
111
|
+
_timeout: HTTP request timeout in seconds
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> import asyncio
|
|
115
|
+
>>> handler = HandlerSlackWebhook()
|
|
116
|
+
>>> alert = ModelSlackAlert(
|
|
117
|
+
... severity=EnumAlertSeverity.ERROR,
|
|
118
|
+
... message="Circuit breaker opened for Consul",
|
|
119
|
+
... title="Infrastructure Alert",
|
|
120
|
+
... )
|
|
121
|
+
>>> # result = await handler.handle(alert)
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
webhook_url: str | None = None,
|
|
127
|
+
http_session: aiohttp.ClientSession | None = None,
|
|
128
|
+
max_retries: int = _DEFAULT_MAX_RETRIES,
|
|
129
|
+
retry_backoff: tuple[float, ...] = _DEFAULT_RETRY_BACKOFF_SECONDS,
|
|
130
|
+
timeout: float = _DEFAULT_TIMEOUT_SECONDS,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Initialize handler with webhook URL and optional HTTP session.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
webhook_url: Slack webhook URL. If not provided, reads from
|
|
136
|
+
SLACK_WEBHOOK_URL environment variable.
|
|
137
|
+
http_session: Optional shared aiohttp ClientSession. If not
|
|
138
|
+
provided, a new session is created per request.
|
|
139
|
+
max_retries: Maximum retry attempts for failed requests.
|
|
140
|
+
Default is 3.
|
|
141
|
+
retry_backoff: Tuple of backoff delays in seconds for each
|
|
142
|
+
retry attempt. Default is (1.0, 2.0, 4.0).
|
|
143
|
+
timeout: HTTP request timeout in seconds. Default is 10.0.
|
|
144
|
+
"""
|
|
145
|
+
self._webhook_url: str = (
|
|
146
|
+
webhook_url if webhook_url else os.getenv("SLACK_WEBHOOK_URL", "")
|
|
147
|
+
)
|
|
148
|
+
self._http_session = http_session
|
|
149
|
+
self._max_retries = max_retries
|
|
150
|
+
self._retry_backoff = retry_backoff
|
|
151
|
+
self._timeout = timeout
|
|
152
|
+
|
|
153
|
+
async def handle(
|
|
154
|
+
self,
|
|
155
|
+
alert: ModelSlackAlert,
|
|
156
|
+
) -> ModelSlackAlertResult:
|
|
157
|
+
"""Execute Slack webhook alert delivery.
|
|
158
|
+
|
|
159
|
+
Formats the alert as a Slack Block Kit message and sends it
|
|
160
|
+
to the configured webhook URL with retry logic.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
alert: Alert payload containing severity, message, and optional
|
|
164
|
+
details.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
ModelSlackAlertResult with:
|
|
168
|
+
- success: True if alert was delivered
|
|
169
|
+
- duration_ms: Time taken for the operation
|
|
170
|
+
- correlation_id: From the input alert
|
|
171
|
+
- error: Sanitized error message (only on failure)
|
|
172
|
+
- error_code: Error code for programmatic handling
|
|
173
|
+
- retry_count: Number of retry attempts made
|
|
174
|
+
|
|
175
|
+
Note:
|
|
176
|
+
This handler does not raise exceptions during normal operation.
|
|
177
|
+
All errors are captured and returned in ModelSlackAlertResult
|
|
178
|
+
to support graceful degradation in alerting scenarios.
|
|
179
|
+
"""
|
|
180
|
+
start_time = time.perf_counter()
|
|
181
|
+
correlation_id = alert.correlation_id
|
|
182
|
+
retry_count = 0
|
|
183
|
+
|
|
184
|
+
# Validate webhook URL is configured
|
|
185
|
+
if not self._webhook_url:
|
|
186
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
187
|
+
return ModelSlackAlertResult(
|
|
188
|
+
success=False,
|
|
189
|
+
duration_ms=duration_ms,
|
|
190
|
+
correlation_id=correlation_id,
|
|
191
|
+
error="SLACK_WEBHOOK_URL not configured",
|
|
192
|
+
error_code="SLACK_NOT_CONFIGURED",
|
|
193
|
+
retry_count=0,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Format the alert as Block Kit message
|
|
197
|
+
slack_payload = self._format_block_kit_message(alert)
|
|
198
|
+
|
|
199
|
+
# Create or use existing HTTP session
|
|
200
|
+
session_created = False
|
|
201
|
+
session = self._http_session
|
|
202
|
+
if session is None:
|
|
203
|
+
session = aiohttp.ClientSession()
|
|
204
|
+
session_created = True
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
return await self._send_with_retry(
|
|
208
|
+
session=session,
|
|
209
|
+
slack_payload=slack_payload,
|
|
210
|
+
correlation_id=correlation_id,
|
|
211
|
+
start_time=start_time,
|
|
212
|
+
)
|
|
213
|
+
finally:
|
|
214
|
+
# Close session if we created it
|
|
215
|
+
if session_created and session is not None:
|
|
216
|
+
await session.close()
|
|
217
|
+
|
|
218
|
+
async def _send_with_retry(
|
|
219
|
+
self,
|
|
220
|
+
session: aiohttp.ClientSession,
|
|
221
|
+
slack_payload: dict[str, object],
|
|
222
|
+
correlation_id: UUID,
|
|
223
|
+
start_time: float,
|
|
224
|
+
) -> ModelSlackAlertResult:
|
|
225
|
+
"""Send webhook with retry logic and rate limit handling.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
session: aiohttp ClientSession for HTTP requests
|
|
229
|
+
slack_payload: Formatted Slack Block Kit payload
|
|
230
|
+
correlation_id: UUID for distributed tracing
|
|
231
|
+
start_time: Performance timer start for duration calculation
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
ModelSlackAlertResult with operation outcome
|
|
235
|
+
"""
|
|
236
|
+
retry_count = 0
|
|
237
|
+
last_error: str | None = None
|
|
238
|
+
last_error_code: str | None = None
|
|
239
|
+
|
|
240
|
+
for attempt in range(self._max_retries + 1):
|
|
241
|
+
try:
|
|
242
|
+
async with session.post(
|
|
243
|
+
self._webhook_url,
|
|
244
|
+
json=slack_payload,
|
|
245
|
+
timeout=aiohttp.ClientTimeout(total=self._timeout),
|
|
246
|
+
) as response:
|
|
247
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
248
|
+
|
|
249
|
+
if response.status == 200:
|
|
250
|
+
# Success
|
|
251
|
+
logger.info(
|
|
252
|
+
"Slack alert delivered successfully",
|
|
253
|
+
extra={
|
|
254
|
+
"correlation_id": str(correlation_id),
|
|
255
|
+
"duration_ms": round(duration_ms, 2),
|
|
256
|
+
"retry_count": retry_count,
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
return ModelSlackAlertResult(
|
|
260
|
+
success=True,
|
|
261
|
+
duration_ms=duration_ms,
|
|
262
|
+
correlation_id=correlation_id,
|
|
263
|
+
retry_count=retry_count,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
elif response.status == 429:
|
|
267
|
+
# Rate limited - retry with backoff
|
|
268
|
+
last_error = "Slack rate limit (429)"
|
|
269
|
+
last_error_code = "SLACK_RATE_LIMITED"
|
|
270
|
+
logger.warning(
|
|
271
|
+
"Slack rate limited, will retry",
|
|
272
|
+
extra={
|
|
273
|
+
"correlation_id": str(correlation_id),
|
|
274
|
+
"attempt": attempt + 1,
|
|
275
|
+
"max_attempts": self._max_retries + 1,
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
elif response.status >= 400:
|
|
280
|
+
# Other HTTP error
|
|
281
|
+
response_text = await response.text()
|
|
282
|
+
last_error = f"HTTP {response.status}: {response_text[:100]}"
|
|
283
|
+
last_error_code = f"SLACK_HTTP_{response.status}"
|
|
284
|
+
logger.warning(
|
|
285
|
+
"Slack webhook error",
|
|
286
|
+
extra={
|
|
287
|
+
"correlation_id": str(correlation_id),
|
|
288
|
+
"status_code": response.status,
|
|
289
|
+
"attempt": attempt + 1,
|
|
290
|
+
},
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
except TimeoutError:
|
|
294
|
+
last_error = "Request timeout"
|
|
295
|
+
last_error_code = "SLACK_TIMEOUT"
|
|
296
|
+
logger.warning(
|
|
297
|
+
"Slack webhook timeout",
|
|
298
|
+
extra={
|
|
299
|
+
"correlation_id": str(correlation_id),
|
|
300
|
+
"timeout_seconds": self._timeout,
|
|
301
|
+
"attempt": attempt + 1,
|
|
302
|
+
},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
except aiohttp.ClientConnectorError as e:
|
|
306
|
+
last_error = sanitize_error_message(e)
|
|
307
|
+
last_error_code = "SLACK_CONNECTION_ERROR"
|
|
308
|
+
logger.warning(
|
|
309
|
+
"Slack webhook connection error",
|
|
310
|
+
extra={
|
|
311
|
+
"correlation_id": str(correlation_id),
|
|
312
|
+
"attempt": attempt + 1,
|
|
313
|
+
},
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
except aiohttp.ClientError as e:
|
|
317
|
+
last_error = sanitize_error_message(e)
|
|
318
|
+
last_error_code = "SLACK_CLIENT_ERROR"
|
|
319
|
+
logger.warning(
|
|
320
|
+
"Slack webhook client error",
|
|
321
|
+
extra={
|
|
322
|
+
"correlation_id": str(correlation_id),
|
|
323
|
+
"attempt": attempt + 1,
|
|
324
|
+
"error_type": type(e).__name__,
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Retry with backoff if we have retries remaining
|
|
329
|
+
if attempt < self._max_retries:
|
|
330
|
+
backoff_index = min(attempt, len(self._retry_backoff) - 1)
|
|
331
|
+
backoff_seconds = self._retry_backoff[backoff_index]
|
|
332
|
+
logger.info(
|
|
333
|
+
"Retrying Slack webhook",
|
|
334
|
+
extra={
|
|
335
|
+
"correlation_id": str(correlation_id),
|
|
336
|
+
"backoff_seconds": backoff_seconds,
|
|
337
|
+
"attempt": attempt + 1,
|
|
338
|
+
},
|
|
339
|
+
)
|
|
340
|
+
await asyncio.sleep(backoff_seconds)
|
|
341
|
+
retry_count += 1
|
|
342
|
+
|
|
343
|
+
# All retries exhausted
|
|
344
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
345
|
+
logger.error(
|
|
346
|
+
"Slack alert delivery failed after retries",
|
|
347
|
+
extra={
|
|
348
|
+
"correlation_id": str(correlation_id),
|
|
349
|
+
"duration_ms": round(duration_ms, 2),
|
|
350
|
+
"retry_count": retry_count,
|
|
351
|
+
"error_code": last_error_code,
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return ModelSlackAlertResult(
|
|
356
|
+
success=False,
|
|
357
|
+
duration_ms=duration_ms,
|
|
358
|
+
correlation_id=correlation_id,
|
|
359
|
+
error=last_error,
|
|
360
|
+
error_code=last_error_code,
|
|
361
|
+
retry_count=retry_count,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def _format_block_kit_message(self, alert: ModelSlackAlert) -> dict[str, object]:
|
|
365
|
+
"""Format alert as Slack Block Kit message.
|
|
366
|
+
|
|
367
|
+
Creates a rich formatted message using Slack's Block Kit API
|
|
368
|
+
with header, message body, and optional detail fields.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
alert: Alert payload to format
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Dict containing Slack Block Kit blocks structure
|
|
375
|
+
"""
|
|
376
|
+
emoji = _SEVERITY_EMOJI.get(alert.severity, ":white_circle:")
|
|
377
|
+
title = alert.title or _SEVERITY_TITLES.get(alert.severity, "Alert")
|
|
378
|
+
|
|
379
|
+
blocks: list[dict[str, object]] = [
|
|
380
|
+
{
|
|
381
|
+
"type": "header",
|
|
382
|
+
"text": {
|
|
383
|
+
"type": "plain_text",
|
|
384
|
+
"text": f"{emoji} {title}",
|
|
385
|
+
"emoji": True,
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{"type": "divider"},
|
|
389
|
+
{
|
|
390
|
+
"type": "section",
|
|
391
|
+
"text": {
|
|
392
|
+
"type": "mrkdwn",
|
|
393
|
+
"text": alert.message[:3000], # Slack limit
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
# Add detail fields if provided
|
|
399
|
+
if alert.details:
|
|
400
|
+
fields: list[dict[str, object]] = []
|
|
401
|
+
for key, value in list(alert.details.items())[:10]: # Limit to 10 fields
|
|
402
|
+
# Convert value to string, truncate if needed
|
|
403
|
+
value_str = str(value)[:100]
|
|
404
|
+
fields.append({"type": "mrkdwn", "text": f"*{key}:*\n{value_str}"})
|
|
405
|
+
|
|
406
|
+
# Slack allows max 10 fields per section
|
|
407
|
+
if fields:
|
|
408
|
+
blocks.append({"type": "section", "fields": fields})
|
|
409
|
+
|
|
410
|
+
# Add correlation ID for traceability
|
|
411
|
+
blocks.append(
|
|
412
|
+
{
|
|
413
|
+
"type": "context",
|
|
414
|
+
"elements": [
|
|
415
|
+
{
|
|
416
|
+
"type": "mrkdwn",
|
|
417
|
+
"text": f"Correlation: `{str(alert.correlation_id)[:16]}...`",
|
|
418
|
+
}
|
|
419
|
+
],
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
return {"blocks": blocks}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
__all__: list[str] = ["HandlerSlackWebhook"]
|
|
@@ -90,6 +90,11 @@ Manifest Persistence Models:
|
|
|
90
90
|
ModelManifestQueryPayload: Payload for manifest.query operation
|
|
91
91
|
ModelManifestQueryResult: Result from manifest.query operation
|
|
92
92
|
ModelManifestMetadata: Lightweight metadata for manifest queries
|
|
93
|
+
|
|
94
|
+
Slack Models:
|
|
95
|
+
EnumAlertSeverity: Alert severity levels (critical, error, warning, info)
|
|
96
|
+
ModelSlackAlert: Input payload for Slack alert operations
|
|
97
|
+
ModelSlackAlertResult: Response from Slack webhook operations
|
|
93
98
|
"""
|
|
94
99
|
|
|
95
100
|
from omnibase_infra.handlers.models.consul import (
|
|
@@ -104,6 +109,7 @@ from omnibase_infra.handlers.models.consul import (
|
|
|
104
109
|
ModelConsulKVPutPayload,
|
|
105
110
|
ModelConsulRegisterPayload,
|
|
106
111
|
)
|
|
112
|
+
from omnibase_infra.handlers.models.enum_alert_severity import EnumAlertSeverity
|
|
107
113
|
from omnibase_infra.handlers.models.http import (
|
|
108
114
|
EnumHttpOperationType,
|
|
109
115
|
HttpPayload,
|
|
@@ -198,6 +204,10 @@ from omnibase_infra.handlers.models.model_qdrant_handler_response import (
|
|
|
198
204
|
ModelQdrantHandlerResponse,
|
|
199
205
|
)
|
|
200
206
|
from omnibase_infra.handlers.models.model_retry_state import ModelRetryState
|
|
207
|
+
from omnibase_infra.handlers.models.model_slack_alert_payload import ModelSlackAlert
|
|
208
|
+
from omnibase_infra.handlers.models.model_slack_alert_result import (
|
|
209
|
+
ModelSlackAlertResult,
|
|
210
|
+
)
|
|
201
211
|
from omnibase_infra.handlers.models.model_vault_handler_response import (
|
|
202
212
|
ModelVaultHandlerResponse,
|
|
203
213
|
)
|
|
@@ -283,4 +293,8 @@ __all__: list[str] = [
|
|
|
283
293
|
"ModelManifestQueryPayload",
|
|
284
294
|
"ModelManifestQueryResult",
|
|
285
295
|
"ModelManifestMetadata",
|
|
296
|
+
# Slack models
|
|
297
|
+
"EnumAlertSeverity",
|
|
298
|
+
"ModelSlackAlert",
|
|
299
|
+
"ModelSlackAlertResult",
|
|
286
300
|
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Alert Severity Enumeration.
|
|
4
|
+
|
|
5
|
+
Defines severity levels for Slack alerts with corresponding
|
|
6
|
+
visual indicators for Block Kit formatting.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EnumAlertSeverity(str, Enum):
|
|
15
|
+
"""Alert severity levels for Slack notifications.
|
|
16
|
+
|
|
17
|
+
Maps to visual indicators in Slack Block Kit messages:
|
|
18
|
+
- CRITICAL: Red circle emoji (immediate attention required)
|
|
19
|
+
- ERROR: Red circle emoji (error occurred)
|
|
20
|
+
- WARNING: Yellow circle emoji (potential issue)
|
|
21
|
+
- INFO: Blue circle emoji (informational)
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
CRITICAL: System-critical issue requiring immediate attention
|
|
25
|
+
ERROR: Error condition that needs investigation
|
|
26
|
+
WARNING: Warning condition that may need attention
|
|
27
|
+
INFO: Informational message
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
CRITICAL = "critical"
|
|
31
|
+
ERROR = "error"
|
|
32
|
+
WARNING = "warning"
|
|
33
|
+
INFO = "info"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["EnumAlertSeverity"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Slack Alert Models Re-export.
|
|
4
|
+
|
|
5
|
+
This module re-exports Slack alert models from their individual files
|
|
6
|
+
to maintain backwards compatibility with existing imports.
|
|
7
|
+
|
|
8
|
+
Models are split per ONEX convention (one model per file):
|
|
9
|
+
- enum_alert_severity.py: EnumAlertSeverity
|
|
10
|
+
- model_slack_alert_payload.py: ModelSlackAlert
|
|
11
|
+
- model_slack_alert_result.py: ModelSlackAlertResult
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from omnibase_infra.handlers.models.enum_alert_severity import EnumAlertSeverity
|
|
15
|
+
from omnibase_infra.handlers.models.model_slack_alert_payload import ModelSlackAlert
|
|
16
|
+
from omnibase_infra.handlers.models.model_slack_alert_result import (
|
|
17
|
+
ModelSlackAlertResult,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"EnumAlertSeverity",
|
|
22
|
+
"ModelSlackAlert",
|
|
23
|
+
"ModelSlackAlertResult",
|
|
24
|
+
]
|