atlas-iac/services/monitoring/vmalert-atlas-availability.yaml
Hermes Agent fbb401e4d5 monitoring(titan): add capacity guardrails for titan-20/21/22 audit
Evidence-led capacity/QoS audit of titan-20 (Hermes LLM fallback/
classifier), titan-21 (STT/TTS), and titan-22 (Jellyfin media-primary,
restored) for t_26da4c88. titan-20/21 are CPU-committed with no safe
headroom (titan-20 at 227 MiB free memory at its 24h worst point);
titan-22 has real idle CPU/RAM but its shared-GPU time-slicing has no
VRAM/engine isolation, so no workload is relocated. Adds alerting for
the sharpest gaps found (titan-20 memory exhaustion, titan-22 CPU/RAM/
GPU-VRAM pressure, Jellyfin CPU throttling, titan-21 CPU pressure) and
two Atlas GPU dashboard panels (VRAM, NVENC/NVDEC utilization) so a
future opportunistic-workload PR or a live transcode incident is
visible without a promql session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 14:42:22 +00:00

565 lines
27 KiB
YAML

# services/monitoring/vmalert-atlas-availability.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: vmalert-atlas-availability-rules
namespace: monitoring
data:
hermes-triage.yaml: |
groups:
- name: hermes.triage
interval: 1m
rules:
# Escalation on its own is NOT worth an email. Every human_required
# incident already files an issue in the failing service's own repo,
# and that issue is the durable artifact. Mailing on every escalation
# made the inbox the loudest and least useful output of the system.
# Two things do deserve mail: a repair that ran and failed, and an
# escalation nobody has touched in hours.
- alert: HermesTriageActionFailed
expr: max by (jenkins_job, build) (ariadne_hermes_triage_incident{status="failed"}) == 1
for: 1m
labels:
severity: critical
service: hermes-triage
annotations:
summary: "Hermes repair FAILED: incident {{ $labels.jenkins_job }}/{{ $labels.build }}"
description: "Ariadne authorized a remediation for {{ $labels.jenkins_job }}/{{ $labels.build }} and it did not succeed, or its rebuild failed. This is the one case where the automation acted and did not leave things better. Jenkins build: https://ci.bstein.dev/job/{{ $labels.jenkins_job }}/{{ $labels.build }}/"
- alert: HermesTriageUnattended
expr: max by (jenkins_job, build) (ariadne_hermes_triage_incident{status="human_required"}) == 1
for: 6h
labels:
severity: warning
service: hermes-triage
annotations:
summary: "Hermes triage unattended 6h: incident {{ $labels.jenkins_job }}/{{ $labels.build }}"
description: "Incident {{ $labels.jenkins_job }}/{{ $labels.build }} has been human_required for six hours. Its issue was filed in the failing service's repository at the time of escalation; this alert only means nobody has resolved it since. Jenkins build: https://ci.bstein.dev/job/{{ $labels.jenkins_job }}/{{ $labels.build }}/"
atlas-availability.yaml: |
groups:
- name: atlas.availability.gateway
interval: 1h
eval_offset: 59m
rules:
- record: atlas:availability:requests_1h
expr: |
sum(increase(
traefik_entrypoint_requests_total{
entrypoint="websecure",
protocol="http",
code=~"[1-5].."
}[1h]
))
labels:
definition: request-v4
scope: atlas
rollup: hourly
- record: atlas:availability:failures_1h
expr: |
sum(increase(
traefik_entrypoint_requests_total{
entrypoint="websecure",
protocol="http",
code=~"5.."
}[1h]
))
labels:
definition: request-v4
scope: atlas
rollup: hourly
- name: atlas.availability.rollup
interval: 1d
eval_offset: 23h59m
rules:
- record: atlas:availability:requests_1d
expr: |
sum(increase(
traefik_entrypoint_requests_total{
entrypoint="websecure",
protocol="http",
code=~"[1-5].."
}[1d]
))
labels:
definition: request-v4
scope: atlas
rollup: daily
- record: atlas:availability:failures_1d
expr: |
sum(increase(
traefik_entrypoint_requests_total{
entrypoint="websecure",
protocol="http",
code=~"5.."
}[1d]
))
labels:
definition: request-v4
scope: atlas
rollup: daily
platform-quality.yaml: |
groups:
- name: platform.quality
# Stay inside the five-minute instant-query lookback, including the
# group's evaluation and remote-write time.
interval: 4m
eval_offset: 1m
rules:
- record: platform_quality:test_case_status:count_1h
expr: |
sum by (suite, branch, test, category, status) (
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category=~"api|chaos|compatibility|component|contract|e2e|integration|manual|performance|regression|reliability|security|smoke|system|ui|unit"}
or label_replace(
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category=""},
"category", "uncategorized", "__name__", ".*"
)
)
labels:
rollup: hourly
- record: platform_quality:test_case_pass_rate:percent_1h
expr: |
100 * (
(
sum by (suite, branch, test, category) (
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category=~"api|chaos|compatibility|component|contract|e2e|integration|manual|performance|regression|reliability|security|smoke|system|ui|unit",status="passed"}
or label_replace(
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category="",status="passed"},
"category", "uncategorized", "__name__", ".*"
)
)
or on(suite, branch, test, category)
(
0 * sum by (suite, branch, test, category) (
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category=~"api|chaos|compatibility|component|contract|e2e|integration|manual|performance|regression|reliability|security|smoke|system|ui|unit",status=~"passed|failed|error|skipped"}
or label_replace(
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category="",status=~"passed|failed|error|skipped"},
"category", "uncategorized", "__name__", ".*"
)
)
)
)
)
/
clamp_min(
sum by (suite, branch, test, category) (
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category=~"api|chaos|compatibility|component|contract|e2e|integration|manual|performance|regression|reliability|security|smoke|system|ui|unit",status=~"passed|failed|error|skipped"}
or label_replace(
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category="",status=~"passed|failed|error|skipped"},
"category", "uncategorized", "__name__", ".*"
)
),
1
)
labels:
rollup: hourly
- record: platform_quality:test_case_health_rate:percent_1h
expr: |
100 * (
(
sum by (suite, branch, test, category) (
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category=~"api|chaos|compatibility|component|contract|e2e|integration|manual|performance|regression|reliability|security|smoke|system|ui|unit",status=~"passed|skipped|not_applicable|na|n/a"}
or label_replace(
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category="",status=~"passed|skipped|not_applicable|na|n/a"},
"category", "uncategorized", "__name__", ".*"
)
)
or on(suite, branch, test, category)
(
0 * sum by (suite, branch, test, category) (
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category=~"api|chaos|compatibility|component|contract|e2e|integration|manual|performance|regression|reliability|security|smoke|system|ui|unit",status=~"passed|failed|error|skipped|not_applicable|na|n/a"}
or label_replace(
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category="",status=~"passed|failed|error|skipped|not_applicable|na|n/a"},
"category", "uncategorized", "__name__", ".*"
)
)
)
)
)
/
clamp_min(
sum by (suite, branch, test, category) (
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category=~"api|chaos|compatibility|component|contract|e2e|integration|manual|performance|regression|reliability|security|smoke|system|ui|unit",status=~"passed|failed|error|skipped|not_applicable|na|n/a"}
or label_replace(
platform_quality_gate_test_case_result{exported_job="platform-quality-ci",branch!="",test!="",test!="__no_test_cases__",category="",status=~"passed|failed|error|skipped|not_applicable|na|n/a"},
"category", "uncategorized", "__name__", ".*"
)
),
1
)
labels:
rollup: hourly
- record: platform_quality:test_category_health_rate:percent_1h
expr: |
avg by (suite, branch, category) (
platform_quality:test_case_health_rate:percent_1h{suite!="",branch!="",test!="",test!="__no_test_cases__",category=~"api|chaos|compatibility|component|contract|e2e|integration|manual|performance|regression|reliability|security|smoke|system|ui|unit"}
)
labels:
rollup: hourly
- record: platform_quality:suite_runs:increase_24h
expr: |
sum by (suite, branch, status) (
(
increase((
max without(instance, job) (
platform_quality_gate_runs_total{exported_job="platform-quality-ci",suite!="",branch!="",status!=""}
)
)[24h:1m])
)
or (
increase((
max without(instance, job) (
platform_quality_gate_runs_total{exported_job="platform-quality-ci",suite!="",branch="",status!=""}
)
)[24h:1m])
* on (suite) group_left(branch)
max by (suite, branch) (platform_quality_gate_build_info{exported_job="platform-quality-ci",suite!="",branch!=""})
)
)
labels:
rollup: hourly
- record: platform_quality:suite_coverage_percent:latest_1h
expr: |
max by (suite, branch) (
{__name__=~"platform_quality_gate_workspace_line_coverage_percent|.*_quality_gate_coverage_percent",exported_job="platform-quality-ci",suite!="",branch!=""}
)
or on (suite, branch)
(
max by (suite) (
{__name__=~"platform_quality_gate_workspace_line_coverage_percent|.*_quality_gate_coverage_percent",exported_job="platform-quality-ci",suite!="",branch=""}
)
* on (suite) group_left(branch)
max by (suite, branch) (platform_quality_gate_build_info{exported_job="platform-quality-ci",suite!="",branch!=""})
)
or on (suite, branch)
(
label_replace(
max by (suite) (
{__name__=~"platform_quality_gate_workspace_line_coverage_percent|.*_quality_gate_coverage_percent",exported_job="platform-quality-ci",suite!="",branch=""}
),
"branch", "unknown", "__name__", ".*"
)
unless on (suite)
max by (suite) (platform_quality_gate_build_info{exported_job="platform-quality-ci",suite!="",branch!=""})
)
labels:
rollup: hourly
- record: platform_quality:suite_source_files_total:latest_1h
expr: |
max by (suite, branch) (
platform_quality_gate_source_files_total{exported_job="platform-quality-ci",suite!="",branch!=""}
)
or on (suite, branch)
(
max by (suite) (
platform_quality_gate_source_files_total{exported_job="platform-quality-ci",suite!="",branch=""}
)
* on (suite) group_left(branch)
max by (suite, branch) (platform_quality_gate_build_info{exported_job="platform-quality-ci",suite!="",branch!=""})
)
or on (suite, branch)
(
label_replace(
max by (suite) (
platform_quality_gate_source_files_total{exported_job="platform-quality-ci",suite!="",branch=""}
),
"branch", "unknown", "__name__", ".*"
)
unless on (suite)
max by (suite) (platform_quality_gate_build_info{exported_job="platform-quality-ci",suite!="",branch!=""})
)
labels:
rollup: hourly
- record: platform_quality:suite_source_lines_over_500_total:latest_1h
expr: |
max by (suite, branch) (
platform_quality_gate_source_lines_over_500_total{exported_job="platform-quality-ci",suite!="",branch!=""}
)
or on (suite, branch)
(
max by (suite) (
platform_quality_gate_source_lines_over_500_total{exported_job="platform-quality-ci",suite!="",branch=""}
)
* on (suite) group_left(branch)
max by (suite, branch) (platform_quality_gate_build_info{exported_job="platform-quality-ci",suite!="",branch!=""})
)
or on (suite, branch)
(
label_replace(
max by (suite) (
platform_quality_gate_source_lines_over_500_total{exported_job="platform-quality-ci",suite!="",branch=""}
),
"branch", "unknown", "__name__", ".*"
)
unless on (suite)
max by (suite) (platform_quality_gate_build_info{exported_job="platform-quality-ci",suite!="",branch!=""})
)
labels:
rollup: hourly
- record: platform_quality:check_status:present_1h
expr: |
sum by (suite, branch, check, status) (
{__name__=~".*_quality_gate_checks_total",exported_job="platform-quality-ci",suite!="",branch!="",check!="",status!=""}
or label_replace(
{__name__=~".*_quality_gate_checks_total",exported_job="platform-quality-ci",suite!="",branch!="",check!="",result!="",status=""},
"status", "$1", "result", "(.*)"
)
or (
{__name__=~".*_quality_gate_checks_total",exported_job="platform-quality-ci",suite!="",branch="",check!="",status!=""}
* on (suite) group_left(branch)
max by (suite, branch) (platform_quality_gate_build_info{exported_job="platform-quality-ci",suite!="",branch!=""})
)
or (
label_replace(
{__name__=~".*_quality_gate_checks_total",exported_job="platform-quality-ci",suite!="",branch="",check!="",result!="",status=""},
"status", "$1", "result", "(.*)"
)
* on (suite) group_left(branch)
max by (suite, branch) (platform_quality_gate_build_info{exported_job="platform-quality-ci",suite!="",branch!=""})
)
or (
label_replace(
{__name__=~".*_quality_gate_checks_total",exported_job="platform-quality-ci",suite!="",branch="",check!="",status!=""},
"branch", "unknown", "__name__", ".*"
)
unless on (suite)
max by (suite) (platform_quality_gate_build_info{exported_job="platform-quality-ci",suite!="",branch!=""})
)
or (
label_replace(
label_replace(
{__name__=~".*_quality_gate_checks_total",exported_job="platform-quality-ci",suite!="",branch="",check!="",result!="",status=""},
"status", "$1", "result", "(.*)"
),
"branch", "unknown", "__name__", ".*"
)
unless on (suite)
max by (suite) (platform_quality_gate_build_info{exported_job="platform-quality-ci",suite!="",branch!=""})
)
)
labels:
rollup: hourly
- record: platform_quality:check_seen_flag:present_1h
expr: |
clamp_max(
max by (suite, branch, check) (
platform_quality:check_status:present_1h{suite!="",branch!="",check!="",status!=""} > 0
),
1
)
labels:
rollup: hourly
- record: platform_quality:check_failed_flag:present_1h
expr: |
clamp_max(
max by (suite, branch, check) (
platform_quality:check_status:present_1h{suite!="",branch!="",check!="",status!~"ok|passed|success|not_applicable|skipped|na|n/a"} > 0
),
1
)
or on (suite, branch, check)
(0 * platform_quality:check_seen_flag:present_1h{suite!="",branch!="",check!=""})
labels:
rollup: hourly
- record: platform_quality:check_healthy_flag:present_1h
expr: |
(
clamp_max(
max by (suite, branch, check) (
platform_quality:check_status:present_1h{suite!="",branch!="",check!="",status=~"ok|passed|success|not_applicable|skipped|na|n/a"} > 0
),
1
)
unless on (suite, branch, check)
(platform_quality:check_failed_flag:present_1h{suite!="",branch!="",check!=""} > 0)
)
or on (suite, branch, check)
(0 * platform_quality:check_seen_flag:present_1h{suite!="",branch!="",check!=""})
labels:
rollup: hourly
- record: platform_quality:sonar_gate_health_percent:latest_1h
expr: |
100 * max by (project_key) (
sonarqube_project_quality_gate_pass{project_key!=""}
)
labels:
rollup: hourly
titan-media-capacity.yaml: |
groups:
- name: atlas.titan-media-capacity
# Covers the titan-20/21/22 evidence audit from t_26da4c88: titan-20
# runs at 227 MiB free at its worst point in a day with no coverage
# at all, and titan-22 is Jellyfin's protected media-primary host —
# any future opportunistic workload there needs contention alerts
# to exist before it ships, not after.
interval: 1m
rules:
- alert: Titan20NodeMemoryCritical
expr: |
(node_memory_MemAvailable_bytes{node="titan-20"} / node_memory_MemTotal_bytes{node="titan-20"}) < 0.05
for: 10m
labels:
severity: critical
service: hermes-ai-llm
annotations:
summary: "titan-20 (Hermes local LLM fallback/classifier) is within 5% of an OOM"
description: "titan-20 hosts the hard-pinned Ollama fallback/classifier for Hermes Chat. Available memory has been under 5% of total for 10m. This node runs with no memory slack under normal load (98%+ used is typical); this alert fires only when it is at real risk of an OOM kill. Do not add workloads to titan-20 to relieve this — see docs/titan_20_21_22_capacity_optimization.md."
- alert: TitanMediaHostCPUPressure
expr: |
(1 - avg(rate(node_cpu_seconds_total{mode="idle",node="titan-22"}[5m]))) > 0.85
for: 10m
labels:
severity: warning
service: jellyfin
annotations:
summary: "titan-22 (Jellyfin media-primary host) CPU busy > 85% for 10m"
description: "titan-22 is normally under 30% CPU (p95, 24h baseline in docs/titan_20_21_22_capacity_optimization.md). Sustained pressure this high risks Jellyfin transcode quality and is the signal to check for a misbehaving opportunistic/scavenger workload."
- alert: TitanMediaHostMemoryPressure
expr: |
(node_memory_MemAvailable_bytes{node="titan-22"} / node_memory_MemTotal_bytes{node="titan-22"}) < 0.15
for: 10m
labels:
severity: warning
service: jellyfin
annotations:
summary: "titan-22 (Jellyfin media-primary host) available memory under 15%"
description: "titan-22 is normally ~90% free (24h baseline in docs/titan_20_21_22_capacity_optimization.md). This protects the reservation budget any future opportunistic/scavenger workload on this node must respect."
- alert: JellyfinCPUThrottled
expr: |
(
rate(container_cpu_cfs_throttled_periods_total{namespace="jellyfin",container="jellyfin"}[5m])
/
rate(container_cpu_cfs_periods_total{namespace="jellyfin",container="jellyfin"}[5m])
) > 0.05
for: 5m
labels:
severity: critical
service: jellyfin
annotations:
summary: "Jellyfin container is CPU-throttled against its own limit"
description: "container_cpu_cfs_throttled_periods_total ratio for jellyfin/jellyfin has been over 5% for 5m. Sustained throttling degrades transcode encode speed and can cause playback stutter. Baseline in docs/titan_20_21_22_capacity_optimization.md measured 0% throttling over 24h; any sustained value here is a regression, most likely node-level contention from a co-scheduled workload."
- alert: TitanMediaGPUMemorySaturation
expr: |
(
(
label_replace(label_replace((DCGM_FI_DEV_FB_USED / (DCGM_FI_DEV_FB_USED + DCGM_FI_DEV_FB_FREE)), "pod", "$1", "Hostname", "(.*)"), "namespace", "monitoring", "", "")
* on(namespace,pod) group_left(node) kube_pod_info{namespace="monitoring"}
)
* on(node) group_left() node_uname_info{node="titan-22"}
) > 0.85
for: 5m
labels:
severity: critical
service: jellyfin
annotations:
summary: "titan-22 GPU VRAM over 85% used"
description: "titan-22's shared RTX 3050 Ti (~8 GiB) is normally under 2% VRAM used (24h peak was 1.27 GiB in the t_26da4c88 audit). This is the concrete failure mode a second shared-GPU tenant (nvidia.com/gpu.shared) would cause for an active Jellyfin hardware transcode; GPU sharing on this node is time-sliced with no VRAM isolation, so this alert is the closest available proxy for contention."
- alert: TitanVoiceHostCPUPressure
expr: |
(1 - avg(rate(node_cpu_seconds_total{mode="idle",node="titan-21"}[5m]))) > 0.90
for: 15m
labels:
severity: warning
service: hermes-voice
annotations:
summary: "titan-21 (Hermes STT/TTS host) CPU busy > 90% for 15m"
description: "titan-21 already runs with 93% of its CPU requested (docs/titan_20_21_22_capacity_optimization.md); this alert catches sustained real contention that would add latency to Hermes Chat's hands-free voice path (feeds t_60d2deb6)."
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vmalert-atlas-availability
namespace: monitoring
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: vmalert-atlas-availability
namespace: monitoring
labels:
app: vmalert-atlas-availability
spec:
replicas: 1
revisionHistoryLimit: 3
selector:
matchLabels:
app: vmalert-atlas-availability
template:
metadata:
labels:
app: vmalert-atlas-availability
annotations:
bstein.dev/rules-revision: "2026-08-05-hermes-triage-alert"
spec:
serviceAccountName: vmalert-atlas-availability
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: NotIn
values:
- titan-22
- titan-24
containers:
- name: vmalert
image: victoriametrics/vmalert:v1.113.0
args:
- -datasource.url=http://victoria-metrics-single-server:8428
- -datasource.queryStep=1h
- -remoteWrite.url=http://victoria-metrics-single-server:8428
- -rule=/etc/vmalert/rules/*.yaml
- -configCheckInterval=30s
- -evaluationInterval=15m
- -notifier.url=http://alertmanager.monitoring.svc.cluster.local:9093
- -httpListenAddr=:8880
ports:
- name: http
containerPort: 8880
readinessProbe:
tcpSocket:
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: http
initialDelaySeconds: 20
periodSeconds: 30
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: rules
mountPath: /etc/vmalert/rules
readOnly: true
volumes:
- name: rules
configMap:
name: vmalert-atlas-availability-rules
---
apiVersion: v1
kind: Service
metadata:
name: vmalert-atlas-availability
namespace: monitoring
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8880"
spec:
selector:
app: vmalert-atlas-availability
ports:
- name: http
port: 8880
targetPort: http