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.
- nthlayer_generate/__init__.py +5 -0
- nthlayer_generate/alertmanager/__init__.py +24 -0
- nthlayer_generate/alertmanager/config.py +265 -0
- nthlayer_generate/alerts/__init__.py +17 -0
- nthlayer_generate/alerts/loader.py +206 -0
- nthlayer_generate/alerts/models.py +190 -0
- nthlayer_generate/alerts/validator.py +258 -0
- nthlayer_generate/cli/__init__.py +52 -0
- nthlayer_generate/cli/alerts.py +374 -0
- nthlayer_generate/cli/apply.py +289 -0
- nthlayer_generate/cli/backstage.py +121 -0
- nthlayer_generate/cli/dashboard.py +148 -0
- nthlayer_generate/cli/dashboard_validate.py +219 -0
- nthlayer_generate/cli/dependencies.py +151 -0
- nthlayer_generate/cli/docs.py +122 -0
- nthlayer_generate/cli/environments.py +473 -0
- nthlayer_generate/cli/formatters/__init__.py +114 -0
- nthlayer_generate/cli/formatters/json_fmt.py +71 -0
- nthlayer_generate/cli/formatters/junit.py +134 -0
- nthlayer_generate/cli/formatters/markdown.py +173 -0
- nthlayer_generate/cli/formatters/models.py +80 -0
- nthlayer_generate/cli/formatters/sarif.py +273 -0
- nthlayer_generate/cli/generate.py +97 -0
- nthlayer_generate/cli/generate_alerts.py +128 -0
- nthlayer_generate/cli/generate_loki.py +139 -0
- nthlayer_generate/cli/identity.py +586 -0
- nthlayer_generate/cli/init.py +490 -0
- nthlayer_generate/cli/lint.py +99 -0
- nthlayer_generate/cli/migrate.py +188 -0
- nthlayer_generate/cli/ownership.py +306 -0
- nthlayer_generate/cli/pagerduty.py +183 -0
- nthlayer_generate/cli/plan.py +254 -0
- nthlayer_generate/cli/recommend_metrics.py +395 -0
- nthlayer_generate/cli/recording_rules.py +126 -0
- nthlayer_generate/cli/setup.py +594 -0
- nthlayer_generate/cli/simulate.py +332 -0
- nthlayer_generate/cli/slo.py +429 -0
- nthlayer_generate/cli/templates.py +68 -0
- nthlayer_generate/cli/topology.py +232 -0
- nthlayer_generate/cli/ux.py +398 -0
- nthlayer_generate/cli/validate.py +132 -0
- nthlayer_generate/cli/validate_metadata.py +184 -0
- nthlayer_generate/cli/validate_slo.py +512 -0
- nthlayer_generate/cli/validate_spec.py +154 -0
- nthlayer_generate/clients/__init__.py +6 -0
- nthlayer_generate/clients/base.py +19 -0
- nthlayer_generate/clients/cortex.py +9 -0
- nthlayer_generate/clients/pagerduty.py +9 -0
- nthlayer_generate/clients/slack.py +11 -0
- nthlayer_generate/config/__init__.py +54 -0
- nthlayer_generate/config/cli.py +545 -0
- nthlayer_generate/config/integrations.py +367 -0
- nthlayer_generate/config/loader.py +311 -0
- nthlayer_generate/config/secrets/__init__.py +490 -0
- nthlayer_generate/config/secrets/backends.py +430 -0
- nthlayer_generate/config/settings.py +78 -0
- nthlayer_generate/core/__init__.py +45 -0
- nthlayer_generate/core/errors.py +54 -0
- nthlayer_generate/core/tiers.py +32 -0
- nthlayer_generate/dashboards/__init__.py +55 -0
- nthlayer_generate/dashboards/builder_sdk.py +717 -0
- nthlayer_generate/dashboards/intents.py +711 -0
- nthlayer_generate/dashboards/manifest_builder.py +190 -0
- nthlayer_generate/dashboards/models.py +257 -0
- nthlayer_generate/dashboards/panel_spec.py +467 -0
- nthlayer_generate/dashboards/resolver.py +372 -0
- nthlayer_generate/dashboards/sdk_adapter.py +497 -0
- nthlayer_generate/dashboards/templates/__init__.py +179 -0
- nthlayer_generate/dashboards/templates/base.py +52 -0
- nthlayer_generate/dashboards/templates/base_intent.py +341 -0
- nthlayer_generate/dashboards/templates/consul_intent.py +92 -0
- nthlayer_generate/dashboards/templates/elasticsearch_intent.py +170 -0
- nthlayer_generate/dashboards/templates/etcd_intent.py +98 -0
- nthlayer_generate/dashboards/templates/haproxy_intent.py +106 -0
- nthlayer_generate/dashboards/templates/http_intent.py +127 -0
- nthlayer_generate/dashboards/templates/kafka_intent.py +74 -0
- nthlayer_generate/dashboards/templates/kubernetes.py +273 -0
- nthlayer_generate/dashboards/templates/mongodb_intent.py +75 -0
- nthlayer_generate/dashboards/templates/mysql_intent.py +93 -0
- nthlayer_generate/dashboards/templates/nats_intent.py +91 -0
- nthlayer_generate/dashboards/templates/nginx_intent.py +117 -0
- nthlayer_generate/dashboards/templates/postgresql_intent.py +208 -0
- nthlayer_generate/dashboards/templates/pulsar_intent.py +91 -0
- nthlayer_generate/dashboards/templates/rabbitmq_intent.py +114 -0
- nthlayer_generate/dashboards/templates/redis_intent.py +161 -0
- nthlayer_generate/dashboards/templates/stream_intent.py +114 -0
- nthlayer_generate/dashboards/templates/traefik_intent.py +95 -0
- nthlayer_generate/dashboards/templates/worker_intent.py +114 -0
- nthlayer_generate/dashboards/validator.py +214 -0
- nthlayer_generate/demo.py +1243 -0
- nthlayer_generate/dependencies/__init__.py +40 -0
- nthlayer_generate/dependencies/discovery.py +484 -0
- nthlayer_generate/dependencies/models.py +24 -0
- nthlayer_generate/dependencies/providers/__init__.py +48 -0
- nthlayer_generate/dependencies/providers/backstage.py +466 -0
- nthlayer_generate/dependencies/providers/base.py +135 -0
- nthlayer_generate/dependencies/providers/consul.py +517 -0
- nthlayer_generate/dependencies/providers/etcd.py +359 -0
- nthlayer_generate/dependencies/providers/kubernetes.py +682 -0
- nthlayer_generate/dependencies/providers/prometheus.py +371 -0
- nthlayer_generate/dependencies/providers/zookeeper.py +398 -0
- nthlayer_generate/discovery/__init__.py +19 -0
- nthlayer_generate/discovery/classifier.py +108 -0
- nthlayer_generate/discovery/client.py +268 -0
- nthlayer_generate/discovery/models.py +61 -0
- nthlayer_generate/domain/__init__.py +3 -0
- nthlayer_generate/domain/models.py +24 -0
- nthlayer_generate/generators/__init__.py +23 -0
- nthlayer_generate/generators/alerts.py +322 -0
- nthlayer_generate/generators/backstage.py +304 -0
- nthlayer_generate/generators/docs.py +619 -0
- nthlayer_generate/generators/sloth.py +468 -0
- nthlayer_generate/identity/__init__.py +47 -0
- nthlayer_generate/identity/models.py +15 -0
- nthlayer_generate/identity/normalizer.py +23 -0
- nthlayer_generate/identity/ownership.py +23 -0
- nthlayer_generate/identity/ownership_providers/__init__.py +27 -0
- nthlayer_generate/identity/ownership_providers/backstage.py +11 -0
- nthlayer_generate/identity/ownership_providers/base.py +15 -0
- nthlayer_generate/identity/ownership_providers/codeowners.py +161 -0
- nthlayer_generate/identity/ownership_providers/declared.py +98 -0
- nthlayer_generate/identity/ownership_providers/kubernetes.py +11 -0
- nthlayer_generate/identity/ownership_providers/pagerduty.py +11 -0
- nthlayer_generate/identity/resolver.py +11 -0
- nthlayer_generate/integrations/__init__.py +10 -0
- nthlayer_generate/integrations/pagerduty.py +338 -0
- nthlayer_generate/logging.py +33 -0
- nthlayer_generate/loki/__init__.py +27 -0
- nthlayer_generate/loki/generator.py +405 -0
- nthlayer_generate/loki/models.py +83 -0
- nthlayer_generate/loki/templates.py +883 -0
- nthlayer_generate/metrics/__init__.py +31 -0
- nthlayer_generate/metrics/discovery.py +145 -0
- nthlayer_generate/metrics/models.py +143 -0
- nthlayer_generate/metrics/recommender.py +256 -0
- nthlayer_generate/metrics/runtime/__init__.py +61 -0
- nthlayer_generate/metrics/runtime/go.py +72 -0
- nthlayer_generate/metrics/runtime/jvm.py +114 -0
- nthlayer_generate/metrics/runtime/nodejs.py +73 -0
- nthlayer_generate/metrics/runtime/python.py +63 -0
- nthlayer_generate/metrics/standards/__init__.py +10 -0
- nthlayer_generate/metrics/standards/aliases.py +140 -0
- nthlayer_generate/metrics/standards/otel_semconv.py +108 -0
- nthlayer_generate/metrics/templates/__init__.py +14 -0
- nthlayer_generate/metrics/templates/api.py +130 -0
- nthlayer_generate/metrics/templates/cache.py +103 -0
- nthlayer_generate/metrics/templates/database_client.py +109 -0
- nthlayer_generate/metrics/templates/gateway.py +138 -0
- nthlayer_generate/metrics/templates/grpc.py +120 -0
- nthlayer_generate/metrics/templates/queue_consumer.py +116 -0
- nthlayer_generate/metrics/templates/registry.py +113 -0
- nthlayer_generate/metrics/templates/worker.py +101 -0
- nthlayer_generate/orchestration/__init__.py +23 -0
- nthlayer_generate/orchestration/engine.py +57 -0
- nthlayer_generate/orchestration/handlers.py +500 -0
- nthlayer_generate/orchestration/plan_builder.py +33 -0
- nthlayer_generate/orchestration/registry.py +63 -0
- nthlayer_generate/orchestration/results.py +67 -0
- nthlayer_generate/orchestrator.py +239 -0
- nthlayer_generate/pagerduty/__init__.py +36 -0
- nthlayer_generate/pagerduty/defaults.py +279 -0
- nthlayer_generate/pagerduty/naming.py +124 -0
- nthlayer_generate/pagerduty/orchestration.py +310 -0
- nthlayer_generate/pagerduty/resources.py +689 -0
- nthlayer_generate/policies/__init__.py +7 -0
- nthlayer_generate/policies/engine.py +70 -0
- nthlayer_generate/policies/models.py +66 -0
- nthlayer_generate/policies/rules.py +144 -0
- nthlayer_generate/providers/__init__.py +16 -0
- nthlayer_generate/providers/__main__.py +4 -0
- nthlayer_generate/providers/base.py +23 -0
- nthlayer_generate/providers/cli.py +81 -0
- nthlayer_generate/providers/grafana.py +21 -0
- nthlayer_generate/providers/lock.py +19 -0
- nthlayer_generate/providers/mimir.py +19 -0
- nthlayer_generate/providers/pagerduty.py +17 -0
- nthlayer_generate/providers/prometheus.py +17 -0
- nthlayer_generate/providers/registry.py +25 -0
- nthlayer_generate/recording_rules/__init__.py +23 -0
- nthlayer_generate/recording_rules/builder.py +365 -0
- nthlayer_generate/recording_rules/manifest_builder.py +634 -0
- nthlayer_generate/recording_rules/models.py +100 -0
- nthlayer_generate/simulate/__init__.py +36 -0
- nthlayer_generate/simulate/engine.py +577 -0
- nthlayer_generate/simulate/graph.py +64 -0
- nthlayer_generate/simulate/models.py +247 -0
- nthlayer_generate/simulate/output.py +97 -0
- nthlayer_generate/simulate/what_if.py +93 -0
- nthlayer_generate/slos/__init__.py +53 -0
- nthlayer_generate/slos/alerts.py +444 -0
- nthlayer_generate/slos/calculator.py +300 -0
- nthlayer_generate/slos/ceiling.py +244 -0
- nthlayer_generate/slos/dependencies.py +365 -0
- nthlayer_generate/slos/models.py +24 -0
- nthlayer_generate/slos/parser.py +167 -0
- nthlayer_generate/slos/pipeline.py +255 -0
- nthlayer_generate/specs/__init__.py +41 -0
- nthlayer_generate/specs/alerting.py +216 -0
- nthlayer_generate/specs/builtin_templates/__init__.py +1 -0
- nthlayer_generate/specs/contracts.py +181 -0
- nthlayer_generate/specs/custom_templates.py +104 -0
- nthlayer_generate/specs/environment_alerts.py +198 -0
- nthlayer_generate/specs/environment_detection.py +204 -0
- nthlayer_generate/specs/environment_gates.py +149 -0
- nthlayer_generate/specs/environment_merger.py +152 -0
- nthlayer_generate/specs/environments.py +198 -0
- nthlayer_generate/specs/helpers.py +94 -0
- nthlayer_generate/specs/loader.py +570 -0
- nthlayer_generate/specs/manifest.py +827 -0
- nthlayer_generate/specs/models.py +168 -0
- nthlayer_generate/specs/opensrm_parser.py +670 -0
- nthlayer_generate/specs/parser.py +270 -0
- nthlayer_generate/specs/template.py +96 -0
- nthlayer_generate/specs/template_loader.py +94 -0
- nthlayer_generate/specs/templates.py +72 -0
- nthlayer_generate/specs/validator.py +263 -0
- nthlayer_generate/specs/variable_substitution.py +116 -0
- nthlayer_generate/topology/__init__.py +33 -0
- nthlayer_generate/topology/enrichment.py +147 -0
- nthlayer_generate/topology/models.py +108 -0
- nthlayer_generate/topology/serializers.py +162 -0
- nthlayer_generate/user_config.py +107 -0
- nthlayer_generate/validation/__init__.py +75 -0
- nthlayer_generate/validation/conftest.py +505 -0
- nthlayer_generate/validation/metadata.py +660 -0
- nthlayer_generate/validation/promql.py +342 -0
- nthlayer_generate/validation/promruval.py +293 -0
- nthlayer_generate-1.0.0.dist-info/METADATA +392 -0
- nthlayer_generate-1.0.0.dist-info/RECORD +232 -0
- nthlayer_generate-1.0.0.dist-info/WHEEL +5 -0
- nthlayer_generate-1.0.0.dist-info/entry_points.txt +3 -0
- nthlayer_generate-1.0.0.dist-info/top_level.txt +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
|