nthlayer-workers 1.0.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.
- nthlayer_workers/__init__.py +5 -0
- nthlayer_workers/cli.py +234 -0
- nthlayer_workers/correlate/__init__.py +1 -0
- nthlayer_workers/correlate/cli.py +847 -0
- nthlayer_workers/correlate/config.py +111 -0
- nthlayer_workers/correlate/correlation/__init__.py +1 -0
- nthlayer_workers/correlate/correlation/changes.py +87 -0
- nthlayer_workers/correlate/correlation/dedup.py +62 -0
- nthlayer_workers/correlate/correlation/engine.py +244 -0
- nthlayer_workers/correlate/correlation/temporal.py +79 -0
- nthlayer_workers/correlate/correlation/topology.py +104 -0
- nthlayer_workers/correlate/ingestion/__init__.py +1 -0
- nthlayer_workers/correlate/ingestion/protocol.py +10 -0
- nthlayer_workers/correlate/ingestion/severity.py +18 -0
- nthlayer_workers/correlate/ingestion/webhook.py +197 -0
- nthlayer_workers/correlate/notifications.py +85 -0
- nthlayer_workers/correlate/prometheus.py +234 -0
- nthlayer_workers/correlate/reasoning.py +375 -0
- nthlayer_workers/correlate/session.py +189 -0
- nthlayer_workers/correlate/snapshot/__init__.py +1 -0
- nthlayer_workers/correlate/snapshot/generator.py +170 -0
- nthlayer_workers/correlate/snapshot/model.py +177 -0
- nthlayer_workers/correlate/snapshot/token.py +14 -0
- nthlayer_workers/correlate/state.py +88 -0
- nthlayer_workers/correlate/store/__init__.py +5 -0
- nthlayer_workers/correlate/store/protocol.py +48 -0
- nthlayer_workers/correlate/store/sqlite.py +443 -0
- nthlayer_workers/correlate/summary.py +180 -0
- nthlayer_workers/correlate/traces/__init__.py +1 -0
- nthlayer_workers/correlate/traces/protocol.py +120 -0
- nthlayer_workers/correlate/traces/tempo.py +667 -0
- nthlayer_workers/correlate/traces/topology.py +39 -0
- nthlayer_workers/correlate/types.py +77 -0
- nthlayer_workers/correlate/worker.py +630 -0
- nthlayer_workers/learn/__init__.py +5 -0
- nthlayer_workers/learn/__main__.py +5 -0
- nthlayer_workers/learn/cli.py +164 -0
- nthlayer_workers/learn/retrospective.py +381 -0
- nthlayer_workers/learn/trends.py +102 -0
- nthlayer_workers/learn/worker.py +366 -0
- nthlayer_workers/measure/__init__.py +3 -0
- nthlayer_workers/measure/__main__.py +5 -0
- nthlayer_workers/measure/_parsing.py +15 -0
- nthlayer_workers/measure/adapters/__init__.py +0 -0
- nthlayer_workers/measure/adapters/_util.py +24 -0
- nthlayer_workers/measure/adapters/devin.py +119 -0
- nthlayer_workers/measure/adapters/gastown.py +88 -0
- nthlayer_workers/measure/adapters/prometheus.py +277 -0
- nthlayer_workers/measure/adapters/protocol.py +20 -0
- nthlayer_workers/measure/adapters/webhook.py +161 -0
- nthlayer_workers/measure/api/__init__.py +0 -0
- nthlayer_workers/measure/api/normalise.py +50 -0
- nthlayer_workers/measure/api/queue.py +243 -0
- nthlayer_workers/measure/api/response.py +51 -0
- nthlayer_workers/measure/api/server.py +504 -0
- nthlayer_workers/measure/calibration/__init__.py +0 -0
- nthlayer_workers/measure/calibration/loop.py +62 -0
- nthlayer_workers/measure/calibration/slos.py +212 -0
- nthlayer_workers/measure/calibration/verdict_calibration.py +31 -0
- nthlayer_workers/measure/cli.py +753 -0
- nthlayer_workers/measure/config.py +191 -0
- nthlayer_workers/measure/detection/__init__.py +6 -0
- nthlayer_workers/measure/detection/detector.py +82 -0
- nthlayer_workers/measure/detection/protocol.py +29 -0
- nthlayer_workers/measure/governance/__init__.py +0 -0
- nthlayer_workers/measure/governance/engine.py +163 -0
- nthlayer_workers/measure/manifest.py +77 -0
- nthlayer_workers/measure/notifications.py +53 -0
- nthlayer_workers/measure/pipeline/__init__.py +0 -0
- nthlayer_workers/measure/pipeline/evaluator.py +155 -0
- nthlayer_workers/measure/pipeline/router.py +160 -0
- nthlayer_workers/measure/store/__init__.py +0 -0
- nthlayer_workers/measure/store/protocol.py +38 -0
- nthlayer_workers/measure/store/sqlite.py +276 -0
- nthlayer_workers/measure/telemetry.py +116 -0
- nthlayer_workers/measure/tiering/__init__.py +0 -0
- nthlayer_workers/measure/tiering/classifier.py +58 -0
- nthlayer_workers/measure/tiering/promotion.py +118 -0
- nthlayer_workers/measure/trends/__init__.py +0 -0
- nthlayer_workers/measure/trends/tracker.py +72 -0
- nthlayer_workers/measure/types.py +75 -0
- nthlayer_workers/measure/worker.py +439 -0
- nthlayer_workers/observe/__init__.py +25 -0
- nthlayer_workers/observe/__main__.py +5 -0
- nthlayer_workers/observe/api/__init__.py +1 -0
- nthlayer_workers/observe/assessment.py +95 -0
- nthlayer_workers/observe/cli.py +737 -0
- nthlayer_workers/observe/config.py +11 -0
- nthlayer_workers/observe/db/__init__.py +1 -0
- nthlayer_workers/observe/decision_records.py +220 -0
- nthlayer_workers/observe/dependencies/__init__.py +18 -0
- nthlayer_workers/observe/dependencies/discovery.py +294 -0
- nthlayer_workers/observe/dependencies/providers/__init__.py +48 -0
- nthlayer_workers/observe/dependencies/providers/backstage.py +467 -0
- nthlayer_workers/observe/dependencies/providers/base.py +76 -0
- nthlayer_workers/observe/dependencies/providers/consul.py +518 -0
- nthlayer_workers/observe/dependencies/providers/etcd.py +360 -0
- nthlayer_workers/observe/dependencies/providers/kubernetes.py +682 -0
- nthlayer_workers/observe/dependencies/providers/prometheus.py +368 -0
- nthlayer_workers/observe/dependencies/providers/zookeeper.py +399 -0
- nthlayer_workers/observe/deployments/__init__.py +1 -0
- nthlayer_workers/observe/discovery/__init__.py +14 -0
- nthlayer_workers/observe/discovery/classifier.py +66 -0
- nthlayer_workers/observe/discovery/client.py +189 -0
- nthlayer_workers/observe/discovery/models.py +53 -0
- nthlayer_workers/observe/drift/__init__.py +26 -0
- nthlayer_workers/observe/drift/analyzer.py +383 -0
- nthlayer_workers/observe/drift/models.py +174 -0
- nthlayer_workers/observe/drift/patterns.py +88 -0
- nthlayer_workers/observe/explanation.py +118 -0
- nthlayer_workers/observe/gate/__init__.py +39 -0
- nthlayer_workers/observe/gate/conditions.py +92 -0
- nthlayer_workers/observe/gate/correlator.py +154 -0
- nthlayer_workers/observe/gate/evaluator.py +192 -0
- nthlayer_workers/observe/gate/policies.py +226 -0
- nthlayer_workers/observe/gate_adapter.py +40 -0
- nthlayer_workers/observe/incident.py +36 -0
- nthlayer_workers/observe/portfolio/__init__.py +17 -0
- nthlayer_workers/observe/portfolio/aggregator.py +168 -0
- nthlayer_workers/observe/portfolio/scorer.py +13 -0
- nthlayer_workers/observe/slo/__init__.py +19 -0
- nthlayer_workers/observe/slo/collector.py +235 -0
- nthlayer_workers/observe/slo/spec_loader.py +40 -0
- nthlayer_workers/observe/sqlite_store.py +152 -0
- nthlayer_workers/observe/store.py +92 -0
- nthlayer_workers/observe/verification/__init__.py +22 -0
- nthlayer_workers/observe/verification/exporter_guidance.py +146 -0
- nthlayer_workers/observe/verification/extractor.py +127 -0
- nthlayer_workers/observe/verification/models.py +101 -0
- nthlayer_workers/observe/verification/verifier.py +111 -0
- nthlayer_workers/observe/worker.py +332 -0
- nthlayer_workers/respond/__init__.py +2 -0
- nthlayer_workers/respond/__main__.py +4 -0
- nthlayer_workers/respond/agents/__init__.py +0 -0
- nthlayer_workers/respond/agents/base.py +556 -0
- nthlayer_workers/respond/agents/communication.py +115 -0
- nthlayer_workers/respond/agents/investigation.py +124 -0
- nthlayer_workers/respond/agents/remediation.py +219 -0
- nthlayer_workers/respond/agents/triage.py +132 -0
- nthlayer_workers/respond/cli.py +772 -0
- nthlayer_workers/respond/config.py +135 -0
- nthlayer_workers/respond/context_store.py +256 -0
- nthlayer_workers/respond/coordinator.py +487 -0
- nthlayer_workers/respond/metrics.py +104 -0
- nthlayer_workers/respond/notification_backends/__init__.py +1 -0
- nthlayer_workers/respond/notification_backends/ntfy_backend.py +158 -0
- nthlayer_workers/respond/notification_backends/protocol.py +59 -0
- nthlayer_workers/respond/notification_backends/slack_backend.py +203 -0
- nthlayer_workers/respond/notification_backends/stdout_backend.py +56 -0
- nthlayer_workers/respond/notifications.py +247 -0
- nthlayer_workers/respond/oncall/__init__.py +1 -0
- nthlayer_workers/respond/oncall/escalation.py +103 -0
- nthlayer_workers/respond/oncall/runner.py +193 -0
- nthlayer_workers/respond/oncall/schedule.py +243 -0
- nthlayer_workers/respond/safe_actions/__init__.py +0 -0
- nthlayer_workers/respond/safe_actions/actions.py +139 -0
- nthlayer_workers/respond/safe_actions/registry.py +171 -0
- nthlayer_workers/respond/safe_actions/webhook.py +194 -0
- nthlayer_workers/respond/server.py +357 -0
- nthlayer_workers/respond/sre/__init__.py +1 -0
- nthlayer_workers/respond/sre/brief.py +175 -0
- nthlayer_workers/respond/sre/delegation.py +101 -0
- nthlayer_workers/respond/sre/post_incident.py +146 -0
- nthlayer_workers/respond/sre/shift_report.py +129 -0
- nthlayer_workers/respond/sre/suppression.py +91 -0
- nthlayer_workers/respond/types.py +109 -0
- nthlayer_workers/respond/verdict_submission.py +56 -0
- nthlayer_workers/respond/worker.py +533 -0
- nthlayer_workers/respond/worker_helpers.py +140 -0
- nthlayer_workers/runner.py +198 -0
- nthlayer_workers-1.0.0.dist-info/METADATA +19 -0
- nthlayer_workers-1.0.0.dist-info/RECORD +175 -0
- nthlayer_workers-1.0.0.dist-info/WHEEL +5 -0
- nthlayer_workers-1.0.0.dist-info/entry_points.txt +2 -0
- nthlayer_workers-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prometheus dependency provider.
|
|
3
|
+
|
|
4
|
+
Discovers service dependencies from Prometheus metrics including:
|
|
5
|
+
- HTTP client/server relationships
|
|
6
|
+
- gRPC service calls
|
|
7
|
+
- Database connections
|
|
8
|
+
- Message queue consumers/producers
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from nthlayer_common.errors import ProviderError
|
|
21
|
+
from nthlayer_common.dependency_models import DependencyType, DiscoveredDependency
|
|
22
|
+
from nthlayer_workers.observe.dependencies.providers.base import (
|
|
23
|
+
BaseDepProvider,
|
|
24
|
+
ProviderHealth,
|
|
25
|
+
deduplicate_dependencies,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PrometheusDepProviderError(ProviderError):
|
|
30
|
+
"""Raised when Prometheus dependency provider encounters an error."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Metric patterns for dependency discovery
|
|
34
|
+
# Each pattern specifies: metric name pattern, source label, target label, dependency type
|
|
35
|
+
DEPENDENCY_PATTERNS = [
|
|
36
|
+
# HTTP client metrics (outbound calls)
|
|
37
|
+
{
|
|
38
|
+
"name": "http_client",
|
|
39
|
+
"queries": [
|
|
40
|
+
'count by (service, target_service) (http_client_requests_total{service=~".+"})',
|
|
41
|
+
'count by (job, target) (http_client_request_duration_seconds_count{job=~".+"})',
|
|
42
|
+
],
|
|
43
|
+
"source_labels": ["service", "job"],
|
|
44
|
+
"target_labels": ["target_service", "target"],
|
|
45
|
+
"dep_type": DependencyType.SERVICE,
|
|
46
|
+
"confidence": 0.9,
|
|
47
|
+
},
|
|
48
|
+
# gRPC client metrics
|
|
49
|
+
{
|
|
50
|
+
"name": "grpc_client",
|
|
51
|
+
"queries": [
|
|
52
|
+
'count by (service, grpc_service) (grpc_client_handled_total{service=~".+"})',
|
|
53
|
+
'count by (job, grpc_method) (grpc_client_started_total{job=~".+"})',
|
|
54
|
+
],
|
|
55
|
+
"source_labels": ["service", "job"],
|
|
56
|
+
"target_labels": ["grpc_service", "grpc_method"],
|
|
57
|
+
"dep_type": DependencyType.SERVICE,
|
|
58
|
+
"confidence": 0.9,
|
|
59
|
+
},
|
|
60
|
+
# Database connections
|
|
61
|
+
{
|
|
62
|
+
"name": "database",
|
|
63
|
+
"queries": [
|
|
64
|
+
'count by (service, database) (db_client_connections{service=~".+"})',
|
|
65
|
+
'count by (job, db_name) (sql_client_queries_total{job=~".+"})',
|
|
66
|
+
'count by (service, addr) (pg_stat_activity_count{service=~".+"})',
|
|
67
|
+
],
|
|
68
|
+
"source_labels": ["service", "job"],
|
|
69
|
+
"target_labels": ["database", "db_name", "addr"],
|
|
70
|
+
"dep_type": DependencyType.DATASTORE,
|
|
71
|
+
"confidence": 0.85,
|
|
72
|
+
},
|
|
73
|
+
# Redis connections
|
|
74
|
+
{
|
|
75
|
+
"name": "redis",
|
|
76
|
+
"queries": [
|
|
77
|
+
'count by (service, redis_addr) (redis_client_commands_total{service=~".+"})',
|
|
78
|
+
'count by (job, addr) (redis_commands_total{job=~".+"})',
|
|
79
|
+
],
|
|
80
|
+
"source_labels": ["service", "job"],
|
|
81
|
+
"target_labels": ["redis_addr", "addr"],
|
|
82
|
+
"dep_type": DependencyType.DATASTORE,
|
|
83
|
+
"confidence": 0.85,
|
|
84
|
+
},
|
|
85
|
+
# Kafka consumers
|
|
86
|
+
{
|
|
87
|
+
"name": "kafka",
|
|
88
|
+
"queries": [
|
|
89
|
+
'count by (service, topic) (kafka_consumer_records_consumed_total{service=~".+"})',
|
|
90
|
+
'count by (consumergroup, topic) (kafka_consumergroup_lag{consumergroup=~".+"})',
|
|
91
|
+
],
|
|
92
|
+
"source_labels": ["service", "consumergroup"],
|
|
93
|
+
"target_labels": ["topic"],
|
|
94
|
+
"dep_type": DependencyType.QUEUE,
|
|
95
|
+
"confidence": 0.85,
|
|
96
|
+
},
|
|
97
|
+
# RabbitMQ consumers
|
|
98
|
+
{
|
|
99
|
+
"name": "rabbitmq",
|
|
100
|
+
"queries": [
|
|
101
|
+
'count by (service, queue) (rabbitmq_consumer_messages_total{service=~".+"})',
|
|
102
|
+
],
|
|
103
|
+
"source_labels": ["service"],
|
|
104
|
+
"target_labels": ["queue"],
|
|
105
|
+
"dep_type": DependencyType.QUEUE,
|
|
106
|
+
"confidence": 0.85,
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class PrometheusDepProvider(BaseDepProvider):
|
|
113
|
+
"""
|
|
114
|
+
Discover dependencies from Prometheus metrics.
|
|
115
|
+
|
|
116
|
+
Configuration:
|
|
117
|
+
url: Prometheus server URL
|
|
118
|
+
username: Optional basic auth username
|
|
119
|
+
password: Optional basic auth password
|
|
120
|
+
timeout: Request timeout in seconds
|
|
121
|
+
|
|
122
|
+
Environment variables:
|
|
123
|
+
NTHLAYER_PROMETHEUS_URL: Prometheus URL
|
|
124
|
+
NTHLAYER_METRICS_USER: Basic auth username
|
|
125
|
+
NTHLAYER_METRICS_PASSWORD: Basic auth password
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
url: str
|
|
129
|
+
username: str | None = None
|
|
130
|
+
password: str | None = None
|
|
131
|
+
timeout: float = 30.0
|
|
132
|
+
|
|
133
|
+
# Optional: custom patterns
|
|
134
|
+
custom_patterns: list[dict[str, Any]] = field(default_factory=list)
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def name(self) -> str:
|
|
138
|
+
return "prometheus"
|
|
139
|
+
|
|
140
|
+
def _get_auth(self) -> tuple[str, str] | None:
|
|
141
|
+
"""Get basic auth tuple if credentials are set."""
|
|
142
|
+
if self.username and self.password:
|
|
143
|
+
return (self.username, self.password)
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
async def _query(self, promql: str) -> list[dict[str, Any]]:
|
|
147
|
+
"""Execute a PromQL instant query."""
|
|
148
|
+
auth = self._get_auth()
|
|
149
|
+
|
|
150
|
+
async with httpx.AsyncClient(auth=auth, timeout=self.timeout) as client:
|
|
151
|
+
response = await client.get(
|
|
152
|
+
f"{self.url.rstrip('/')}/api/v1/query",
|
|
153
|
+
params={"query": promql},
|
|
154
|
+
)
|
|
155
|
+
response.raise_for_status()
|
|
156
|
+
result = response.json()
|
|
157
|
+
|
|
158
|
+
if result.get("status") != "success":
|
|
159
|
+
raise PrometheusDepProviderError(
|
|
160
|
+
f"Prometheus query failed: {result.get('error', 'Unknown')}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return result.get("data", {}).get("result", [])
|
|
164
|
+
|
|
165
|
+
async def discover(self, service: str) -> list[DiscoveredDependency]:
|
|
166
|
+
"""Discover dependencies for a service from Prometheus metrics."""
|
|
167
|
+
deps: list[DiscoveredDependency] = []
|
|
168
|
+
patterns = DEPENDENCY_PATTERNS + self.custom_patterns
|
|
169
|
+
|
|
170
|
+
for pattern in patterns:
|
|
171
|
+
for query in pattern.get("queries", []):
|
|
172
|
+
try:
|
|
173
|
+
# Modify query to filter by service
|
|
174
|
+
filtered_query = self._add_service_filter(
|
|
175
|
+
query, service, pattern["source_labels"]
|
|
176
|
+
)
|
|
177
|
+
results = await self._query(filtered_query)
|
|
178
|
+
|
|
179
|
+
for result in results:
|
|
180
|
+
metric = result.get("metric", {})
|
|
181
|
+
|
|
182
|
+
# Extract source and target from labels
|
|
183
|
+
source = self._extract_label(metric, pattern["source_labels"])
|
|
184
|
+
target = self._extract_label(metric, pattern["target_labels"])
|
|
185
|
+
|
|
186
|
+
if source and target:
|
|
187
|
+
deps.append(
|
|
188
|
+
DiscoveredDependency(
|
|
189
|
+
source_service=source,
|
|
190
|
+
target_service=target,
|
|
191
|
+
provider=self.name,
|
|
192
|
+
dep_type=pattern["dep_type"],
|
|
193
|
+
confidence=pattern["confidence"],
|
|
194
|
+
metadata={
|
|
195
|
+
"pattern": pattern["name"],
|
|
196
|
+
"query": filtered_query,
|
|
197
|
+
"labels": metric,
|
|
198
|
+
},
|
|
199
|
+
raw_source=source,
|
|
200
|
+
raw_target=target,
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
except Exception:
|
|
204
|
+
# Continue with other patterns on error
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
# Deduplicate dependencies
|
|
208
|
+
return deduplicate_dependencies(deps)
|
|
209
|
+
|
|
210
|
+
async def discover_downstream(self, service: str) -> list[DiscoveredDependency]:
|
|
211
|
+
"""Discover services that call this service (downstream dependents)."""
|
|
212
|
+
deps: list[DiscoveredDependency] = []
|
|
213
|
+
patterns = DEPENDENCY_PATTERNS + self.custom_patterns
|
|
214
|
+
|
|
215
|
+
for pattern in patterns:
|
|
216
|
+
for query in pattern.get("queries", []):
|
|
217
|
+
try:
|
|
218
|
+
# Modify query to filter by target service
|
|
219
|
+
filtered_query = self._add_target_filter(
|
|
220
|
+
query, service, pattern["target_labels"]
|
|
221
|
+
)
|
|
222
|
+
results = await self._query(filtered_query)
|
|
223
|
+
|
|
224
|
+
for result in results:
|
|
225
|
+
metric = result.get("metric", {})
|
|
226
|
+
|
|
227
|
+
source = self._extract_label(metric, pattern["source_labels"])
|
|
228
|
+
target = self._extract_label(metric, pattern["target_labels"])
|
|
229
|
+
|
|
230
|
+
if source and target:
|
|
231
|
+
deps.append(
|
|
232
|
+
DiscoveredDependency(
|
|
233
|
+
source_service=source,
|
|
234
|
+
target_service=target,
|
|
235
|
+
provider=self.name,
|
|
236
|
+
dep_type=pattern["dep_type"],
|
|
237
|
+
confidence=pattern["confidence"],
|
|
238
|
+
metadata={
|
|
239
|
+
"pattern": pattern["name"],
|
|
240
|
+
"direction": "downstream",
|
|
241
|
+
},
|
|
242
|
+
raw_source=source,
|
|
243
|
+
raw_target=target,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
except Exception:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
return deduplicate_dependencies(deps)
|
|
250
|
+
|
|
251
|
+
def _add_service_filter(self, query: str, service: str, source_labels: list[str]) -> str:
|
|
252
|
+
"""Add service filter to query."""
|
|
253
|
+
# Find a source label that's in the query and add filter
|
|
254
|
+
for label in source_labels:
|
|
255
|
+
if f"{label}=" in query or f"{label}~" in query:
|
|
256
|
+
# Replace the regex matcher with exact match
|
|
257
|
+
query = re.sub(
|
|
258
|
+
rf'{label}[=~]+"[^"]*"',
|
|
259
|
+
f'{label}="{service}"',
|
|
260
|
+
query,
|
|
261
|
+
)
|
|
262
|
+
return query
|
|
263
|
+
|
|
264
|
+
# If no label found in query, add filter
|
|
265
|
+
label = source_labels[0]
|
|
266
|
+
# Insert before closing brace
|
|
267
|
+
if "{" in query:
|
|
268
|
+
query = query.replace("}", f',{label}="{service}"}}')
|
|
269
|
+
return query
|
|
270
|
+
|
|
271
|
+
def _add_target_filter(self, query: str, service: str, target_labels: list[str]) -> str:
|
|
272
|
+
"""Add target filter to query."""
|
|
273
|
+
for label in target_labels:
|
|
274
|
+
if f"{label}=" in query or f"{label}~" in query:
|
|
275
|
+
query = re.sub(
|
|
276
|
+
rf'{label}[=~]+"[^"]*"',
|
|
277
|
+
f'{label}="{service}"',
|
|
278
|
+
query,
|
|
279
|
+
)
|
|
280
|
+
return query
|
|
281
|
+
|
|
282
|
+
label = target_labels[0]
|
|
283
|
+
if "{" in query:
|
|
284
|
+
query = query.replace("}", f',{label}="{service}"}}')
|
|
285
|
+
return query
|
|
286
|
+
|
|
287
|
+
def _extract_label(self, metric: dict[str, str], labels: list[str]) -> str | None:
|
|
288
|
+
"""Extract first available label from metric."""
|
|
289
|
+
for label in labels:
|
|
290
|
+
if label in metric and metric[label]:
|
|
291
|
+
return metric[label]
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
async def list_services(self) -> list[str]:
|
|
295
|
+
"""List all services with metrics in Prometheus."""
|
|
296
|
+
services: set[str] = set()
|
|
297
|
+
|
|
298
|
+
# Query for common service label names
|
|
299
|
+
label_queries = [
|
|
300
|
+
'count by (service) ({__name__=~".+", service=~".+"})',
|
|
301
|
+
'count by (job) ({__name__=~".+", job=~".+"})',
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
for query in label_queries:
|
|
305
|
+
try:
|
|
306
|
+
results = await self._query(query)
|
|
307
|
+
for result in results:
|
|
308
|
+
metric = result.get("metric", {})
|
|
309
|
+
for label in ["service", "job"]:
|
|
310
|
+
if label in metric and metric[label]:
|
|
311
|
+
services.add(metric[label])
|
|
312
|
+
except Exception:
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
return sorted(services)
|
|
316
|
+
|
|
317
|
+
async def health_check(self) -> ProviderHealth:
|
|
318
|
+
"""Check Prometheus connectivity."""
|
|
319
|
+
start = time.time()
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
auth = self._get_auth()
|
|
323
|
+
async with httpx.AsyncClient(auth=auth, timeout=10.0) as client:
|
|
324
|
+
response = await client.get(f"{self.url.rstrip('/')}/api/v1/status/config")
|
|
325
|
+
latency = (time.time() - start) * 1000
|
|
326
|
+
|
|
327
|
+
if response.status_code == 200:
|
|
328
|
+
return ProviderHealth(
|
|
329
|
+
healthy=True,
|
|
330
|
+
message="Connected to Prometheus",
|
|
331
|
+
latency_ms=latency,
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
return ProviderHealth(
|
|
335
|
+
healthy=False,
|
|
336
|
+
message=f"Prometheus returned {response.status_code}",
|
|
337
|
+
latency_ms=latency,
|
|
338
|
+
)
|
|
339
|
+
except httpx.TimeoutException:
|
|
340
|
+
return ProviderHealth(
|
|
341
|
+
healthy=False,
|
|
342
|
+
message="Prometheus connection timed out",
|
|
343
|
+
)
|
|
344
|
+
except Exception as e:
|
|
345
|
+
return ProviderHealth(
|
|
346
|
+
healthy=False,
|
|
347
|
+
message=f"Prometheus connection failed: {e}",
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
async def get_service_attributes(self, service: str) -> dict:
|
|
351
|
+
"""Get service attributes from Prometheus labels."""
|
|
352
|
+
attributes: dict[str, Any] = {}
|
|
353
|
+
|
|
354
|
+
# Query for service info metrics
|
|
355
|
+
try:
|
|
356
|
+
results = await self._query(f'{{service="{service}", __name__=~".*info.*"}}')
|
|
357
|
+
|
|
358
|
+
if results:
|
|
359
|
+
# Collect all labels from info metrics
|
|
360
|
+
for result in results:
|
|
361
|
+
metric = result.get("metric", {})
|
|
362
|
+
for key, value in metric.items():
|
|
363
|
+
if key not in ["__name__", "service"] and value:
|
|
364
|
+
attributes[key] = value
|
|
365
|
+
except Exception:
|
|
366
|
+
pass
|
|
367
|
+
|
|
368
|
+
return attributes
|