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.
Files changed (175) hide show
  1. nthlayer_workers/__init__.py +5 -0
  2. nthlayer_workers/cli.py +234 -0
  3. nthlayer_workers/correlate/__init__.py +1 -0
  4. nthlayer_workers/correlate/cli.py +847 -0
  5. nthlayer_workers/correlate/config.py +111 -0
  6. nthlayer_workers/correlate/correlation/__init__.py +1 -0
  7. nthlayer_workers/correlate/correlation/changes.py +87 -0
  8. nthlayer_workers/correlate/correlation/dedup.py +62 -0
  9. nthlayer_workers/correlate/correlation/engine.py +244 -0
  10. nthlayer_workers/correlate/correlation/temporal.py +79 -0
  11. nthlayer_workers/correlate/correlation/topology.py +104 -0
  12. nthlayer_workers/correlate/ingestion/__init__.py +1 -0
  13. nthlayer_workers/correlate/ingestion/protocol.py +10 -0
  14. nthlayer_workers/correlate/ingestion/severity.py +18 -0
  15. nthlayer_workers/correlate/ingestion/webhook.py +197 -0
  16. nthlayer_workers/correlate/notifications.py +85 -0
  17. nthlayer_workers/correlate/prometheus.py +234 -0
  18. nthlayer_workers/correlate/reasoning.py +375 -0
  19. nthlayer_workers/correlate/session.py +189 -0
  20. nthlayer_workers/correlate/snapshot/__init__.py +1 -0
  21. nthlayer_workers/correlate/snapshot/generator.py +170 -0
  22. nthlayer_workers/correlate/snapshot/model.py +177 -0
  23. nthlayer_workers/correlate/snapshot/token.py +14 -0
  24. nthlayer_workers/correlate/state.py +88 -0
  25. nthlayer_workers/correlate/store/__init__.py +5 -0
  26. nthlayer_workers/correlate/store/protocol.py +48 -0
  27. nthlayer_workers/correlate/store/sqlite.py +443 -0
  28. nthlayer_workers/correlate/summary.py +180 -0
  29. nthlayer_workers/correlate/traces/__init__.py +1 -0
  30. nthlayer_workers/correlate/traces/protocol.py +120 -0
  31. nthlayer_workers/correlate/traces/tempo.py +667 -0
  32. nthlayer_workers/correlate/traces/topology.py +39 -0
  33. nthlayer_workers/correlate/types.py +77 -0
  34. nthlayer_workers/correlate/worker.py +630 -0
  35. nthlayer_workers/learn/__init__.py +5 -0
  36. nthlayer_workers/learn/__main__.py +5 -0
  37. nthlayer_workers/learn/cli.py +164 -0
  38. nthlayer_workers/learn/retrospective.py +381 -0
  39. nthlayer_workers/learn/trends.py +102 -0
  40. nthlayer_workers/learn/worker.py +366 -0
  41. nthlayer_workers/measure/__init__.py +3 -0
  42. nthlayer_workers/measure/__main__.py +5 -0
  43. nthlayer_workers/measure/_parsing.py +15 -0
  44. nthlayer_workers/measure/adapters/__init__.py +0 -0
  45. nthlayer_workers/measure/adapters/_util.py +24 -0
  46. nthlayer_workers/measure/adapters/devin.py +119 -0
  47. nthlayer_workers/measure/adapters/gastown.py +88 -0
  48. nthlayer_workers/measure/adapters/prometheus.py +277 -0
  49. nthlayer_workers/measure/adapters/protocol.py +20 -0
  50. nthlayer_workers/measure/adapters/webhook.py +161 -0
  51. nthlayer_workers/measure/api/__init__.py +0 -0
  52. nthlayer_workers/measure/api/normalise.py +50 -0
  53. nthlayer_workers/measure/api/queue.py +243 -0
  54. nthlayer_workers/measure/api/response.py +51 -0
  55. nthlayer_workers/measure/api/server.py +504 -0
  56. nthlayer_workers/measure/calibration/__init__.py +0 -0
  57. nthlayer_workers/measure/calibration/loop.py +62 -0
  58. nthlayer_workers/measure/calibration/slos.py +212 -0
  59. nthlayer_workers/measure/calibration/verdict_calibration.py +31 -0
  60. nthlayer_workers/measure/cli.py +753 -0
  61. nthlayer_workers/measure/config.py +191 -0
  62. nthlayer_workers/measure/detection/__init__.py +6 -0
  63. nthlayer_workers/measure/detection/detector.py +82 -0
  64. nthlayer_workers/measure/detection/protocol.py +29 -0
  65. nthlayer_workers/measure/governance/__init__.py +0 -0
  66. nthlayer_workers/measure/governance/engine.py +163 -0
  67. nthlayer_workers/measure/manifest.py +77 -0
  68. nthlayer_workers/measure/notifications.py +53 -0
  69. nthlayer_workers/measure/pipeline/__init__.py +0 -0
  70. nthlayer_workers/measure/pipeline/evaluator.py +155 -0
  71. nthlayer_workers/measure/pipeline/router.py +160 -0
  72. nthlayer_workers/measure/store/__init__.py +0 -0
  73. nthlayer_workers/measure/store/protocol.py +38 -0
  74. nthlayer_workers/measure/store/sqlite.py +276 -0
  75. nthlayer_workers/measure/telemetry.py +116 -0
  76. nthlayer_workers/measure/tiering/__init__.py +0 -0
  77. nthlayer_workers/measure/tiering/classifier.py +58 -0
  78. nthlayer_workers/measure/tiering/promotion.py +118 -0
  79. nthlayer_workers/measure/trends/__init__.py +0 -0
  80. nthlayer_workers/measure/trends/tracker.py +72 -0
  81. nthlayer_workers/measure/types.py +75 -0
  82. nthlayer_workers/measure/worker.py +439 -0
  83. nthlayer_workers/observe/__init__.py +25 -0
  84. nthlayer_workers/observe/__main__.py +5 -0
  85. nthlayer_workers/observe/api/__init__.py +1 -0
  86. nthlayer_workers/observe/assessment.py +95 -0
  87. nthlayer_workers/observe/cli.py +737 -0
  88. nthlayer_workers/observe/config.py +11 -0
  89. nthlayer_workers/observe/db/__init__.py +1 -0
  90. nthlayer_workers/observe/decision_records.py +220 -0
  91. nthlayer_workers/observe/dependencies/__init__.py +18 -0
  92. nthlayer_workers/observe/dependencies/discovery.py +294 -0
  93. nthlayer_workers/observe/dependencies/providers/__init__.py +48 -0
  94. nthlayer_workers/observe/dependencies/providers/backstage.py +467 -0
  95. nthlayer_workers/observe/dependencies/providers/base.py +76 -0
  96. nthlayer_workers/observe/dependencies/providers/consul.py +518 -0
  97. nthlayer_workers/observe/dependencies/providers/etcd.py +360 -0
  98. nthlayer_workers/observe/dependencies/providers/kubernetes.py +682 -0
  99. nthlayer_workers/observe/dependencies/providers/prometheus.py +368 -0
  100. nthlayer_workers/observe/dependencies/providers/zookeeper.py +399 -0
  101. nthlayer_workers/observe/deployments/__init__.py +1 -0
  102. nthlayer_workers/observe/discovery/__init__.py +14 -0
  103. nthlayer_workers/observe/discovery/classifier.py +66 -0
  104. nthlayer_workers/observe/discovery/client.py +189 -0
  105. nthlayer_workers/observe/discovery/models.py +53 -0
  106. nthlayer_workers/observe/drift/__init__.py +26 -0
  107. nthlayer_workers/observe/drift/analyzer.py +383 -0
  108. nthlayer_workers/observe/drift/models.py +174 -0
  109. nthlayer_workers/observe/drift/patterns.py +88 -0
  110. nthlayer_workers/observe/explanation.py +118 -0
  111. nthlayer_workers/observe/gate/__init__.py +39 -0
  112. nthlayer_workers/observe/gate/conditions.py +92 -0
  113. nthlayer_workers/observe/gate/correlator.py +154 -0
  114. nthlayer_workers/observe/gate/evaluator.py +192 -0
  115. nthlayer_workers/observe/gate/policies.py +226 -0
  116. nthlayer_workers/observe/gate_adapter.py +40 -0
  117. nthlayer_workers/observe/incident.py +36 -0
  118. nthlayer_workers/observe/portfolio/__init__.py +17 -0
  119. nthlayer_workers/observe/portfolio/aggregator.py +168 -0
  120. nthlayer_workers/observe/portfolio/scorer.py +13 -0
  121. nthlayer_workers/observe/slo/__init__.py +19 -0
  122. nthlayer_workers/observe/slo/collector.py +235 -0
  123. nthlayer_workers/observe/slo/spec_loader.py +40 -0
  124. nthlayer_workers/observe/sqlite_store.py +152 -0
  125. nthlayer_workers/observe/store.py +92 -0
  126. nthlayer_workers/observe/verification/__init__.py +22 -0
  127. nthlayer_workers/observe/verification/exporter_guidance.py +146 -0
  128. nthlayer_workers/observe/verification/extractor.py +127 -0
  129. nthlayer_workers/observe/verification/models.py +101 -0
  130. nthlayer_workers/observe/verification/verifier.py +111 -0
  131. nthlayer_workers/observe/worker.py +332 -0
  132. nthlayer_workers/respond/__init__.py +2 -0
  133. nthlayer_workers/respond/__main__.py +4 -0
  134. nthlayer_workers/respond/agents/__init__.py +0 -0
  135. nthlayer_workers/respond/agents/base.py +556 -0
  136. nthlayer_workers/respond/agents/communication.py +115 -0
  137. nthlayer_workers/respond/agents/investigation.py +124 -0
  138. nthlayer_workers/respond/agents/remediation.py +219 -0
  139. nthlayer_workers/respond/agents/triage.py +132 -0
  140. nthlayer_workers/respond/cli.py +772 -0
  141. nthlayer_workers/respond/config.py +135 -0
  142. nthlayer_workers/respond/context_store.py +256 -0
  143. nthlayer_workers/respond/coordinator.py +487 -0
  144. nthlayer_workers/respond/metrics.py +104 -0
  145. nthlayer_workers/respond/notification_backends/__init__.py +1 -0
  146. nthlayer_workers/respond/notification_backends/ntfy_backend.py +158 -0
  147. nthlayer_workers/respond/notification_backends/protocol.py +59 -0
  148. nthlayer_workers/respond/notification_backends/slack_backend.py +203 -0
  149. nthlayer_workers/respond/notification_backends/stdout_backend.py +56 -0
  150. nthlayer_workers/respond/notifications.py +247 -0
  151. nthlayer_workers/respond/oncall/__init__.py +1 -0
  152. nthlayer_workers/respond/oncall/escalation.py +103 -0
  153. nthlayer_workers/respond/oncall/runner.py +193 -0
  154. nthlayer_workers/respond/oncall/schedule.py +243 -0
  155. nthlayer_workers/respond/safe_actions/__init__.py +0 -0
  156. nthlayer_workers/respond/safe_actions/actions.py +139 -0
  157. nthlayer_workers/respond/safe_actions/registry.py +171 -0
  158. nthlayer_workers/respond/safe_actions/webhook.py +194 -0
  159. nthlayer_workers/respond/server.py +357 -0
  160. nthlayer_workers/respond/sre/__init__.py +1 -0
  161. nthlayer_workers/respond/sre/brief.py +175 -0
  162. nthlayer_workers/respond/sre/delegation.py +101 -0
  163. nthlayer_workers/respond/sre/post_incident.py +146 -0
  164. nthlayer_workers/respond/sre/shift_report.py +129 -0
  165. nthlayer_workers/respond/sre/suppression.py +91 -0
  166. nthlayer_workers/respond/types.py +109 -0
  167. nthlayer_workers/respond/verdict_submission.py +56 -0
  168. nthlayer_workers/respond/worker.py +533 -0
  169. nthlayer_workers/respond/worker_helpers.py +140 -0
  170. nthlayer_workers/runner.py +198 -0
  171. nthlayer_workers-1.0.0.dist-info/METADATA +19 -0
  172. nthlayer_workers-1.0.0.dist-info/RECORD +175 -0
  173. nthlayer_workers-1.0.0.dist-info/WHEEL +5 -0
  174. nthlayer_workers-1.0.0.dist-info/entry_points.txt +2 -0
  175. 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