nthlayer-generate 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 (232) hide show
  1. nthlayer_generate/__init__.py +5 -0
  2. nthlayer_generate/alertmanager/__init__.py +24 -0
  3. nthlayer_generate/alertmanager/config.py +265 -0
  4. nthlayer_generate/alerts/__init__.py +17 -0
  5. nthlayer_generate/alerts/loader.py +206 -0
  6. nthlayer_generate/alerts/models.py +190 -0
  7. nthlayer_generate/alerts/validator.py +258 -0
  8. nthlayer_generate/cli/__init__.py +52 -0
  9. nthlayer_generate/cli/alerts.py +374 -0
  10. nthlayer_generate/cli/apply.py +289 -0
  11. nthlayer_generate/cli/backstage.py +121 -0
  12. nthlayer_generate/cli/dashboard.py +148 -0
  13. nthlayer_generate/cli/dashboard_validate.py +219 -0
  14. nthlayer_generate/cli/dependencies.py +151 -0
  15. nthlayer_generate/cli/docs.py +122 -0
  16. nthlayer_generate/cli/environments.py +473 -0
  17. nthlayer_generate/cli/formatters/__init__.py +114 -0
  18. nthlayer_generate/cli/formatters/json_fmt.py +71 -0
  19. nthlayer_generate/cli/formatters/junit.py +134 -0
  20. nthlayer_generate/cli/formatters/markdown.py +173 -0
  21. nthlayer_generate/cli/formatters/models.py +80 -0
  22. nthlayer_generate/cli/formatters/sarif.py +273 -0
  23. nthlayer_generate/cli/generate.py +97 -0
  24. nthlayer_generate/cli/generate_alerts.py +128 -0
  25. nthlayer_generate/cli/generate_loki.py +139 -0
  26. nthlayer_generate/cli/identity.py +586 -0
  27. nthlayer_generate/cli/init.py +490 -0
  28. nthlayer_generate/cli/lint.py +99 -0
  29. nthlayer_generate/cli/migrate.py +188 -0
  30. nthlayer_generate/cli/ownership.py +306 -0
  31. nthlayer_generate/cli/pagerduty.py +183 -0
  32. nthlayer_generate/cli/plan.py +254 -0
  33. nthlayer_generate/cli/recommend_metrics.py +395 -0
  34. nthlayer_generate/cli/recording_rules.py +126 -0
  35. nthlayer_generate/cli/setup.py +594 -0
  36. nthlayer_generate/cli/simulate.py +332 -0
  37. nthlayer_generate/cli/slo.py +429 -0
  38. nthlayer_generate/cli/templates.py +68 -0
  39. nthlayer_generate/cli/topology.py +232 -0
  40. nthlayer_generate/cli/ux.py +398 -0
  41. nthlayer_generate/cli/validate.py +132 -0
  42. nthlayer_generate/cli/validate_metadata.py +184 -0
  43. nthlayer_generate/cli/validate_slo.py +512 -0
  44. nthlayer_generate/cli/validate_spec.py +154 -0
  45. nthlayer_generate/clients/__init__.py +6 -0
  46. nthlayer_generate/clients/base.py +19 -0
  47. nthlayer_generate/clients/cortex.py +9 -0
  48. nthlayer_generate/clients/pagerduty.py +9 -0
  49. nthlayer_generate/clients/slack.py +11 -0
  50. nthlayer_generate/config/__init__.py +54 -0
  51. nthlayer_generate/config/cli.py +545 -0
  52. nthlayer_generate/config/integrations.py +367 -0
  53. nthlayer_generate/config/loader.py +311 -0
  54. nthlayer_generate/config/secrets/__init__.py +490 -0
  55. nthlayer_generate/config/secrets/backends.py +430 -0
  56. nthlayer_generate/config/settings.py +78 -0
  57. nthlayer_generate/core/__init__.py +45 -0
  58. nthlayer_generate/core/errors.py +54 -0
  59. nthlayer_generate/core/tiers.py +32 -0
  60. nthlayer_generate/dashboards/__init__.py +55 -0
  61. nthlayer_generate/dashboards/builder_sdk.py +717 -0
  62. nthlayer_generate/dashboards/intents.py +711 -0
  63. nthlayer_generate/dashboards/manifest_builder.py +190 -0
  64. nthlayer_generate/dashboards/models.py +257 -0
  65. nthlayer_generate/dashboards/panel_spec.py +467 -0
  66. nthlayer_generate/dashboards/resolver.py +372 -0
  67. nthlayer_generate/dashboards/sdk_adapter.py +497 -0
  68. nthlayer_generate/dashboards/templates/__init__.py +179 -0
  69. nthlayer_generate/dashboards/templates/base.py +52 -0
  70. nthlayer_generate/dashboards/templates/base_intent.py +341 -0
  71. nthlayer_generate/dashboards/templates/consul_intent.py +92 -0
  72. nthlayer_generate/dashboards/templates/elasticsearch_intent.py +170 -0
  73. nthlayer_generate/dashboards/templates/etcd_intent.py +98 -0
  74. nthlayer_generate/dashboards/templates/haproxy_intent.py +106 -0
  75. nthlayer_generate/dashboards/templates/http_intent.py +127 -0
  76. nthlayer_generate/dashboards/templates/kafka_intent.py +74 -0
  77. nthlayer_generate/dashboards/templates/kubernetes.py +273 -0
  78. nthlayer_generate/dashboards/templates/mongodb_intent.py +75 -0
  79. nthlayer_generate/dashboards/templates/mysql_intent.py +93 -0
  80. nthlayer_generate/dashboards/templates/nats_intent.py +91 -0
  81. nthlayer_generate/dashboards/templates/nginx_intent.py +117 -0
  82. nthlayer_generate/dashboards/templates/postgresql_intent.py +208 -0
  83. nthlayer_generate/dashboards/templates/pulsar_intent.py +91 -0
  84. nthlayer_generate/dashboards/templates/rabbitmq_intent.py +114 -0
  85. nthlayer_generate/dashboards/templates/redis_intent.py +161 -0
  86. nthlayer_generate/dashboards/templates/stream_intent.py +114 -0
  87. nthlayer_generate/dashboards/templates/traefik_intent.py +95 -0
  88. nthlayer_generate/dashboards/templates/worker_intent.py +114 -0
  89. nthlayer_generate/dashboards/validator.py +214 -0
  90. nthlayer_generate/demo.py +1243 -0
  91. nthlayer_generate/dependencies/__init__.py +40 -0
  92. nthlayer_generate/dependencies/discovery.py +484 -0
  93. nthlayer_generate/dependencies/models.py +24 -0
  94. nthlayer_generate/dependencies/providers/__init__.py +48 -0
  95. nthlayer_generate/dependencies/providers/backstage.py +466 -0
  96. nthlayer_generate/dependencies/providers/base.py +135 -0
  97. nthlayer_generate/dependencies/providers/consul.py +517 -0
  98. nthlayer_generate/dependencies/providers/etcd.py +359 -0
  99. nthlayer_generate/dependencies/providers/kubernetes.py +682 -0
  100. nthlayer_generate/dependencies/providers/prometheus.py +371 -0
  101. nthlayer_generate/dependencies/providers/zookeeper.py +398 -0
  102. nthlayer_generate/discovery/__init__.py +19 -0
  103. nthlayer_generate/discovery/classifier.py +108 -0
  104. nthlayer_generate/discovery/client.py +268 -0
  105. nthlayer_generate/discovery/models.py +61 -0
  106. nthlayer_generate/domain/__init__.py +3 -0
  107. nthlayer_generate/domain/models.py +24 -0
  108. nthlayer_generate/generators/__init__.py +23 -0
  109. nthlayer_generate/generators/alerts.py +322 -0
  110. nthlayer_generate/generators/backstage.py +304 -0
  111. nthlayer_generate/generators/docs.py +619 -0
  112. nthlayer_generate/generators/sloth.py +468 -0
  113. nthlayer_generate/identity/__init__.py +47 -0
  114. nthlayer_generate/identity/models.py +15 -0
  115. nthlayer_generate/identity/normalizer.py +23 -0
  116. nthlayer_generate/identity/ownership.py +23 -0
  117. nthlayer_generate/identity/ownership_providers/__init__.py +27 -0
  118. nthlayer_generate/identity/ownership_providers/backstage.py +11 -0
  119. nthlayer_generate/identity/ownership_providers/base.py +15 -0
  120. nthlayer_generate/identity/ownership_providers/codeowners.py +161 -0
  121. nthlayer_generate/identity/ownership_providers/declared.py +98 -0
  122. nthlayer_generate/identity/ownership_providers/kubernetes.py +11 -0
  123. nthlayer_generate/identity/ownership_providers/pagerduty.py +11 -0
  124. nthlayer_generate/identity/resolver.py +11 -0
  125. nthlayer_generate/integrations/__init__.py +10 -0
  126. nthlayer_generate/integrations/pagerduty.py +338 -0
  127. nthlayer_generate/logging.py +33 -0
  128. nthlayer_generate/loki/__init__.py +27 -0
  129. nthlayer_generate/loki/generator.py +405 -0
  130. nthlayer_generate/loki/models.py +83 -0
  131. nthlayer_generate/loki/templates.py +883 -0
  132. nthlayer_generate/metrics/__init__.py +31 -0
  133. nthlayer_generate/metrics/discovery.py +145 -0
  134. nthlayer_generate/metrics/models.py +143 -0
  135. nthlayer_generate/metrics/recommender.py +256 -0
  136. nthlayer_generate/metrics/runtime/__init__.py +61 -0
  137. nthlayer_generate/metrics/runtime/go.py +72 -0
  138. nthlayer_generate/metrics/runtime/jvm.py +114 -0
  139. nthlayer_generate/metrics/runtime/nodejs.py +73 -0
  140. nthlayer_generate/metrics/runtime/python.py +63 -0
  141. nthlayer_generate/metrics/standards/__init__.py +10 -0
  142. nthlayer_generate/metrics/standards/aliases.py +140 -0
  143. nthlayer_generate/metrics/standards/otel_semconv.py +108 -0
  144. nthlayer_generate/metrics/templates/__init__.py +14 -0
  145. nthlayer_generate/metrics/templates/api.py +130 -0
  146. nthlayer_generate/metrics/templates/cache.py +103 -0
  147. nthlayer_generate/metrics/templates/database_client.py +109 -0
  148. nthlayer_generate/metrics/templates/gateway.py +138 -0
  149. nthlayer_generate/metrics/templates/grpc.py +120 -0
  150. nthlayer_generate/metrics/templates/queue_consumer.py +116 -0
  151. nthlayer_generate/metrics/templates/registry.py +113 -0
  152. nthlayer_generate/metrics/templates/worker.py +101 -0
  153. nthlayer_generate/orchestration/__init__.py +23 -0
  154. nthlayer_generate/orchestration/engine.py +57 -0
  155. nthlayer_generate/orchestration/handlers.py +500 -0
  156. nthlayer_generate/orchestration/plan_builder.py +33 -0
  157. nthlayer_generate/orchestration/registry.py +63 -0
  158. nthlayer_generate/orchestration/results.py +67 -0
  159. nthlayer_generate/orchestrator.py +239 -0
  160. nthlayer_generate/pagerduty/__init__.py +36 -0
  161. nthlayer_generate/pagerduty/defaults.py +279 -0
  162. nthlayer_generate/pagerduty/naming.py +124 -0
  163. nthlayer_generate/pagerduty/orchestration.py +310 -0
  164. nthlayer_generate/pagerduty/resources.py +689 -0
  165. nthlayer_generate/policies/__init__.py +7 -0
  166. nthlayer_generate/policies/engine.py +70 -0
  167. nthlayer_generate/policies/models.py +66 -0
  168. nthlayer_generate/policies/rules.py +144 -0
  169. nthlayer_generate/providers/__init__.py +16 -0
  170. nthlayer_generate/providers/__main__.py +4 -0
  171. nthlayer_generate/providers/base.py +23 -0
  172. nthlayer_generate/providers/cli.py +81 -0
  173. nthlayer_generate/providers/grafana.py +21 -0
  174. nthlayer_generate/providers/lock.py +19 -0
  175. nthlayer_generate/providers/mimir.py +19 -0
  176. nthlayer_generate/providers/pagerduty.py +17 -0
  177. nthlayer_generate/providers/prometheus.py +17 -0
  178. nthlayer_generate/providers/registry.py +25 -0
  179. nthlayer_generate/recording_rules/__init__.py +23 -0
  180. nthlayer_generate/recording_rules/builder.py +365 -0
  181. nthlayer_generate/recording_rules/manifest_builder.py +634 -0
  182. nthlayer_generate/recording_rules/models.py +100 -0
  183. nthlayer_generate/simulate/__init__.py +36 -0
  184. nthlayer_generate/simulate/engine.py +577 -0
  185. nthlayer_generate/simulate/graph.py +64 -0
  186. nthlayer_generate/simulate/models.py +247 -0
  187. nthlayer_generate/simulate/output.py +97 -0
  188. nthlayer_generate/simulate/what_if.py +93 -0
  189. nthlayer_generate/slos/__init__.py +53 -0
  190. nthlayer_generate/slos/alerts.py +444 -0
  191. nthlayer_generate/slos/calculator.py +300 -0
  192. nthlayer_generate/slos/ceiling.py +244 -0
  193. nthlayer_generate/slos/dependencies.py +365 -0
  194. nthlayer_generate/slos/models.py +24 -0
  195. nthlayer_generate/slos/parser.py +167 -0
  196. nthlayer_generate/slos/pipeline.py +255 -0
  197. nthlayer_generate/specs/__init__.py +41 -0
  198. nthlayer_generate/specs/alerting.py +216 -0
  199. nthlayer_generate/specs/builtin_templates/__init__.py +1 -0
  200. nthlayer_generate/specs/contracts.py +181 -0
  201. nthlayer_generate/specs/custom_templates.py +104 -0
  202. nthlayer_generate/specs/environment_alerts.py +198 -0
  203. nthlayer_generate/specs/environment_detection.py +204 -0
  204. nthlayer_generate/specs/environment_gates.py +149 -0
  205. nthlayer_generate/specs/environment_merger.py +152 -0
  206. nthlayer_generate/specs/environments.py +198 -0
  207. nthlayer_generate/specs/helpers.py +94 -0
  208. nthlayer_generate/specs/loader.py +570 -0
  209. nthlayer_generate/specs/manifest.py +827 -0
  210. nthlayer_generate/specs/models.py +168 -0
  211. nthlayer_generate/specs/opensrm_parser.py +670 -0
  212. nthlayer_generate/specs/parser.py +270 -0
  213. nthlayer_generate/specs/template.py +96 -0
  214. nthlayer_generate/specs/template_loader.py +94 -0
  215. nthlayer_generate/specs/templates.py +72 -0
  216. nthlayer_generate/specs/validator.py +263 -0
  217. nthlayer_generate/specs/variable_substitution.py +116 -0
  218. nthlayer_generate/topology/__init__.py +33 -0
  219. nthlayer_generate/topology/enrichment.py +147 -0
  220. nthlayer_generate/topology/models.py +108 -0
  221. nthlayer_generate/topology/serializers.py +162 -0
  222. nthlayer_generate/user_config.py +107 -0
  223. nthlayer_generate/validation/__init__.py +75 -0
  224. nthlayer_generate/validation/conftest.py +505 -0
  225. nthlayer_generate/validation/metadata.py +660 -0
  226. nthlayer_generate/validation/promql.py +342 -0
  227. nthlayer_generate/validation/promruval.py +293 -0
  228. nthlayer_generate-1.0.0.dist-info/METADATA +392 -0
  229. nthlayer_generate-1.0.0.dist-info/RECORD +232 -0
  230. nthlayer_generate-1.0.0.dist-info/WHEEL +5 -0
  231. nthlayer_generate-1.0.0.dist-info/entry_points.txt +3 -0
  232. nthlayer_generate-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ """NthLayer control plane package."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,24 @@
1
+ """
2
+ Alertmanager configuration generation.
3
+
4
+ Generates Alertmanager configuration with PagerDuty receivers,
5
+ routing rules, and inhibition rules based on service definitions.
6
+ """
7
+
8
+ from nthlayer_generate.alertmanager.config import (
9
+ AlertmanagerConfig,
10
+ InhibitRule,
11
+ PagerDutyReceiver,
12
+ Receiver,
13
+ Route,
14
+ generate_alertmanager_config,
15
+ )
16
+
17
+ __all__ = [
18
+ "AlertmanagerConfig",
19
+ "generate_alertmanager_config",
20
+ "InhibitRule",
21
+ "PagerDutyReceiver",
22
+ "Receiver",
23
+ "Route",
24
+ ]
@@ -0,0 +1,265 @@
1
+ """
2
+ Alertmanager configuration generator.
3
+
4
+ Generates Alertmanager configuration with PagerDuty receivers
5
+ and routing rules based on service definitions.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import yaml
15
+
16
+
17
+ @dataclass
18
+ class PagerDutyReceiver:
19
+ """PagerDuty receiver configuration."""
20
+
21
+ service_key: str # Integration key from PagerDuty
22
+ severity: str = "error" # PagerDuty severity
23
+ class_: str = "" # Alert class
24
+ component: str = "" # Component that generated the alert
25
+ group: str = "" # Logical grouping
26
+ send_resolved: bool = True
27
+ url: str = "https://events.pagerduty.com/v2/enqueue"
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ """Convert to Alertmanager config format."""
31
+ config: dict[str, Any] = {
32
+ "service_key": self.service_key,
33
+ "send_resolved": self.send_resolved,
34
+ }
35
+ if self.url != "https://events.pagerduty.com/v2/enqueue":
36
+ config["url"] = self.url
37
+ if self.severity:
38
+ config["severity"] = self.severity
39
+ if self.class_:
40
+ config["class"] = self.class_
41
+ if self.component:
42
+ config["component"] = self.component
43
+ if self.group:
44
+ config["group"] = self.group
45
+ return config
46
+
47
+
48
+ @dataclass
49
+ class Receiver:
50
+ """Alertmanager receiver configuration."""
51
+
52
+ name: str
53
+ pagerduty_configs: list[PagerDutyReceiver] = field(default_factory=list)
54
+
55
+ def to_dict(self) -> dict[str, Any]:
56
+ """Convert to Alertmanager config format."""
57
+ result: dict[str, Any] = {"name": self.name}
58
+ if self.pagerduty_configs:
59
+ result["pagerduty_configs"] = [pd.to_dict() for pd in self.pagerduty_configs]
60
+ return result
61
+
62
+
63
+ @dataclass
64
+ class Route:
65
+ """Alertmanager routing rule."""
66
+
67
+ receiver: str
68
+ group_by: list[str] = field(default_factory=lambda: ["alertname", "service"])
69
+ group_wait: str = "30s"
70
+ group_interval: str = "5m"
71
+ repeat_interval: str = "4h"
72
+ matchers: list[str] = field(default_factory=list)
73
+ routes: list[Route] = field(default_factory=list)
74
+ continue_: bool = False
75
+
76
+ def to_dict(self) -> dict[str, Any]:
77
+ """Convert to Alertmanager config format."""
78
+ result: dict[str, Any] = {"receiver": self.receiver}
79
+ if self.group_by:
80
+ result["group_by"] = self.group_by
81
+ if self.group_wait:
82
+ result["group_wait"] = self.group_wait
83
+ if self.group_interval:
84
+ result["group_interval"] = self.group_interval
85
+ if self.repeat_interval:
86
+ result["repeat_interval"] = self.repeat_interval
87
+ if self.matchers:
88
+ result["matchers"] = self.matchers
89
+ if self.routes:
90
+ result["routes"] = [r.to_dict() for r in self.routes]
91
+ if self.continue_:
92
+ result["continue"] = True
93
+ return result
94
+
95
+
96
+ @dataclass
97
+ class InhibitRule:
98
+ """Alertmanager inhibition rule."""
99
+
100
+ source_matchers: list[str] = field(default_factory=list)
101
+ target_matchers: list[str] = field(default_factory=list)
102
+ equal: list[str] = field(default_factory=lambda: ["alertname", "service"])
103
+
104
+ def to_dict(self) -> dict[str, Any]:
105
+ """Convert to Alertmanager config format."""
106
+ return {
107
+ "source_matchers": self.source_matchers,
108
+ "target_matchers": self.target_matchers,
109
+ "equal": self.equal,
110
+ }
111
+
112
+
113
+ @dataclass
114
+ class AlertmanagerConfig:
115
+ """Complete Alertmanager configuration."""
116
+
117
+ route: Route
118
+ receivers: list[Receiver] = field(default_factory=list)
119
+ inhibit_rules: list[InhibitRule] = field(default_factory=list)
120
+
121
+ def to_dict(self) -> dict[str, Any]:
122
+ """Convert to Alertmanager config format."""
123
+ return {
124
+ "route": self.route.to_dict(),
125
+ "receivers": [r.to_dict() for r in self.receivers],
126
+ "inhibit_rules": [ir.to_dict() for ir in self.inhibit_rules],
127
+ }
128
+
129
+ def to_yaml(self) -> str:
130
+ """Convert to YAML string."""
131
+ return yaml.dump(self.to_dict(), default_flow_style=False, sort_keys=False)
132
+
133
+ def write(self, path: Path) -> None:
134
+ """Write configuration to file."""
135
+ path.parent.mkdir(parents=True, exist_ok=True)
136
+ with open(path, "w") as f:
137
+ f.write("# Alertmanager configuration generated by NthLayer\n")
138
+ f.write("# Do not edit manually - regenerate with: nthlayer apply\n\n")
139
+ f.write(self.to_yaml())
140
+
141
+
142
+ def generate_alertmanager_config(
143
+ service_name: str,
144
+ team: str,
145
+ pagerduty_integration_key: str,
146
+ support_model: str = "self",
147
+ tier: str = "medium",
148
+ sre_integration_key: str | None = None,
149
+ ) -> AlertmanagerConfig:
150
+ """
151
+ Generate Alertmanager configuration for a service.
152
+
153
+ Args:
154
+ service_name: Name of the service
155
+ team: Team owning the service
156
+ pagerduty_integration_key: PagerDuty Events API v2 integration key
157
+ support_model: Support model (self, shared, sre, business_hours)
158
+ tier: Service tier (critical, high, medium, low)
159
+ sre_integration_key: Optional SRE PagerDuty integration key for routing
160
+
161
+ Returns:
162
+ AlertmanagerConfig with receivers and routes
163
+ """
164
+ receivers: list[Receiver] = []
165
+ routes: list[Route] = []
166
+
167
+ # Create team receiver
168
+ team_receiver = Receiver(
169
+ name=f"{team}-pagerduty",
170
+ pagerduty_configs=[
171
+ PagerDutyReceiver(
172
+ service_key=pagerduty_integration_key,
173
+ severity=_tier_to_severity(tier),
174
+ component=service_name,
175
+ group=team,
176
+ )
177
+ ],
178
+ )
179
+ receivers.append(team_receiver)
180
+
181
+ # Create SRE receiver if integration key provided
182
+ if sre_integration_key:
183
+ sre_receiver = Receiver(
184
+ name="sre-pagerduty",
185
+ pagerduty_configs=[
186
+ PagerDutyReceiver(
187
+ service_key=sre_integration_key,
188
+ severity="critical",
189
+ component=service_name,
190
+ group="sre",
191
+ )
192
+ ],
193
+ )
194
+ receivers.append(sre_receiver)
195
+
196
+ # Add route for SRE routing label
197
+ sre_route = Route(
198
+ receiver="sre-pagerduty",
199
+ matchers=['routing="sre"'],
200
+ group_by=["alertname", "service"],
201
+ )
202
+ routes.append(sre_route)
203
+
204
+ # Create default route to team
205
+ default_route = Route(
206
+ receiver=f"{team}-pagerduty",
207
+ matchers=[f'service="{service_name}"'],
208
+ group_by=["alertname", "service"],
209
+ group_wait=_tier_to_group_wait(tier),
210
+ repeat_interval=_tier_to_repeat_interval(tier),
211
+ )
212
+ routes.append(default_route)
213
+
214
+ # Create root route
215
+ root_route = Route(
216
+ receiver=f"{team}-pagerduty",
217
+ group_by=["alertname", "service"],
218
+ routes=routes,
219
+ )
220
+
221
+ # Create inhibit rules
222
+ inhibit_rules = [
223
+ # Critical inhibits warning for same alert
224
+ InhibitRule(
225
+ source_matchers=['severity="critical"'],
226
+ target_matchers=['severity="warning"'],
227
+ equal=["alertname", "service"],
228
+ ),
229
+ ]
230
+
231
+ return AlertmanagerConfig(
232
+ route=root_route,
233
+ receivers=receivers,
234
+ inhibit_rules=inhibit_rules,
235
+ )
236
+
237
+
238
+ def _tier_to_severity(tier: str) -> str:
239
+ """Map service tier to PagerDuty severity."""
240
+ return {
241
+ "critical": "critical",
242
+ "high": "error",
243
+ "medium": "warning",
244
+ "low": "info",
245
+ }.get(tier.lower(), "warning")
246
+
247
+
248
+ def _tier_to_group_wait(tier: str) -> str:
249
+ """Map service tier to group wait time."""
250
+ return {
251
+ "critical": "10s",
252
+ "high": "30s",
253
+ "medium": "1m",
254
+ "low": "5m",
255
+ }.get(tier.lower(), "30s")
256
+
257
+
258
+ def _tier_to_repeat_interval(tier: str) -> str:
259
+ """Map service tier to repeat interval."""
260
+ return {
261
+ "critical": "1h",
262
+ "high": "2h",
263
+ "medium": "4h",
264
+ "low": "12h",
265
+ }.get(tier.lower(), "4h")
@@ -0,0 +1,17 @@
1
+ """
2
+ NthLayer Alerts Module
3
+
4
+ Automatic alert generation from awesome-prometheus-alerts templates.
5
+
6
+ Features:
7
+ - 580+ battle-tested alerting rules
8
+ - Automatic dependency detection
9
+ - Tier-based filtering
10
+ - Multi-platform support (Prometheus, Datadog, CloudWatch)
11
+ """
12
+
13
+ from .loader import AlertTemplateLoader
14
+ from .models import AlertRule
15
+ from .validator import ValidationResult, validate_and_fix_alert
16
+
17
+ __all__ = ["AlertRule", "AlertTemplateLoader", "ValidationResult", "validate_and_fix_alert"]
@@ -0,0 +1,206 @@
1
+ """
2
+ Alert Template Loader
3
+
4
+ Loads alerting rules from awesome-prometheus-alerts templates.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional
9
+
10
+ import structlog
11
+ import yaml
12
+
13
+ from .models import AlertRule
14
+
15
+ logger = structlog.get_logger()
16
+
17
+
18
+ class AlertTemplateLoader:
19
+ """
20
+ Load alerting rules from awesome-prometheus-alerts templates.
21
+
22
+ Templates are organized by technology:
23
+ templates/databases/postgres.yaml
24
+ templates/databases/mysql.yaml
25
+ templates/proxies/nginx.yaml
26
+ etc.
27
+
28
+ Usage:
29
+ loader = AlertTemplateLoader()
30
+ alerts = loader.load_technology("postgres")
31
+ # Returns List[AlertRule] for PostgreSQL
32
+ """
33
+
34
+ def __init__(self, templates_dir: Optional[Path] = None):
35
+ """
36
+ Initialize loader.
37
+
38
+ Args:
39
+ templates_dir: Path to templates directory.
40
+ Defaults to ./templates relative to this file.
41
+ """
42
+ if templates_dir is None:
43
+ templates_dir = Path(__file__).parent / "templates"
44
+
45
+ self.templates_dir = Path(templates_dir)
46
+ self.cache: Dict[str, List[AlertRule]] = {}
47
+
48
+ logger.debug("alert_template_loader_init", templates_dir=str(self.templates_dir))
49
+
50
+ def load_technology(self, technology: str) -> List[AlertRule]:
51
+ """
52
+ Load alerts for a specific technology.
53
+
54
+ Args:
55
+ technology: Technology name (e.g., "postgres", "redis", "nginx")
56
+
57
+ Returns:
58
+ List of AlertRule objects for that technology
59
+
60
+ Raises:
61
+ ValueError: If technology not found
62
+ """
63
+ # Check cache first
64
+ if technology in self.cache:
65
+ logger.debug(f"Returning cached alerts for {technology}")
66
+ return self.cache[technology]
67
+
68
+ # Find template file
69
+ template_file = self._find_template(technology)
70
+ if not template_file:
71
+ logger.warning(f"No template found for technology: {technology}")
72
+ return []
73
+
74
+ # Load and parse
75
+ alerts = self._load_from_file(template_file, technology)
76
+
77
+ # Cache results
78
+ self.cache[technology] = alerts
79
+
80
+ logger.debug(
81
+ "alerts_loaded", technology=technology, count=len(alerts), template=str(template_file)
82
+ )
83
+ return alerts
84
+
85
+ def _find_template(self, technology: str) -> Optional[Path]:
86
+ """
87
+ Find template file for a technology.
88
+
89
+ Searches across all categories (databases, proxies, orchestrators, etc.)
90
+ with fuzzy matching (postgres, postgresql, pg all match postgres.yaml)
91
+ """
92
+ # Normalize technology name
93
+ tech_lower = technology.lower()
94
+
95
+ # Common aliases
96
+ aliases = {
97
+ "pg": "postgres",
98
+ "postgresql": "postgres",
99
+ "mysql": "mysql",
100
+ "mariadb": "mysql",
101
+ "mongo": "mongodb",
102
+ "mongodb": "mongodb",
103
+ "k8s": "kubernetes",
104
+ "kubernetes": "kubernetes",
105
+ }
106
+
107
+ # Try alias first
108
+ if tech_lower in aliases:
109
+ tech_lower = aliases[tech_lower]
110
+
111
+ # Search for exact match
112
+ for category_dir in self.templates_dir.iterdir():
113
+ if not category_dir.is_dir():
114
+ continue
115
+
116
+ template_file = category_dir / f"{tech_lower}.yaml"
117
+ if template_file.exists():
118
+ return template_file
119
+
120
+ # Try fuzzy match
121
+ for category_dir in self.templates_dir.iterdir():
122
+ if not category_dir.is_dir():
123
+ continue
124
+
125
+ for template_file in category_dir.glob("*.yaml"):
126
+ if tech_lower in template_file.stem.lower():
127
+ return template_file
128
+
129
+ return None
130
+
131
+ def _load_from_file(self, template_file: Path, technology: str) -> List[AlertRule]:
132
+ """
133
+ Load alerts from a YAML template file.
134
+
135
+ Expected format (awesome-prometheus-alerts):
136
+ groups:
137
+ - name: <technology>
138
+ rules:
139
+ - alert: AlertName
140
+ expr: <promql>
141
+ for: 5m
142
+ labels:
143
+ severity: critical
144
+ annotations:
145
+ summary: Alert summary
146
+ description: Alert description
147
+ """
148
+ try:
149
+ with open(template_file) as f:
150
+ data = yaml.safe_load(f)
151
+ except (FileNotFoundError, yaml.YAMLError, OSError) as e:
152
+ logger.error(f"Failed to load {template_file}: {e}")
153
+ return []
154
+
155
+ # Determine category from parent directory
156
+ category = template_file.parent.name
157
+
158
+ # Parse rules
159
+ alerts = []
160
+ for group in data.get("groups", []):
161
+ for rule_dict in group.get("rules", []):
162
+ if "alert" not in rule_dict:
163
+ # Skip recording rules (not alerts)
164
+ continue
165
+
166
+ try:
167
+ alert = AlertRule.from_dict(rule_dict, technology=technology, category=category)
168
+ alerts.append(alert)
169
+ except (KeyError, ValueError, TypeError) as e:
170
+ logger.warning(f"Failed to parse alert {rule_dict.get('alert')}: {e}")
171
+
172
+ return alerts
173
+
174
+ def list_available_technologies(self) -> List[str]:
175
+ """
176
+ List all available technologies with templates.
177
+
178
+ Returns:
179
+ List of technology names (e.g., ["postgres", "redis", "nginx"])
180
+ """
181
+ technologies = []
182
+
183
+ for category_dir in self.templates_dir.iterdir():
184
+ if not category_dir.is_dir():
185
+ continue
186
+
187
+ for template_file in category_dir.glob("*.yaml"):
188
+ tech = template_file.stem
189
+ technologies.append(tech)
190
+
191
+ return sorted(technologies)
192
+
193
+ def get_category_for_technology(self, technology: str) -> Optional[str]:
194
+ """
195
+ Get the category (databases, proxies, etc.) for a technology.
196
+
197
+ Args:
198
+ technology: Technology name
199
+
200
+ Returns:
201
+ Category name or None if not found
202
+ """
203
+ template_file = self._find_template(technology)
204
+ if template_file:
205
+ return template_file.parent.name
206
+ return None