tatara Grafana alerting -> emergency-brainstorm - Design¶
Date: 2026-06-24 Repos: infra/terraform (grafana) [primary], tatara-helmfile [delivery], infra/helmfile [none] Status: approved design, pre-plan Scope: single milestone, end-to-end. Builds on the shipped 2026-06-19 Grafana incident-response feature (docs/superpowers/specs/2026-06-19-grafana-incident-response-design.md).
Problem¶
The tatara platform exposes Prometheus metrics and structured JSON logs, but nothing turns a degraded tatara component into action. We want Grafana alerts (workload-generic + tatara-specific, metric + log-based) that, when they fire, are fed back into tatara via the existing operator webhook so tatara reacts: an incident Task (emergency-brainstorm) investigates and files a brainstorming GitHub issue that enters the normal lifecycle.
The reactive machinery already exists and is live (operator baa7be0): a webhook receiver POST /operator/webhooks/{project}/grafana (bearer-gated, firing-only, dedup) spawns a project-scoped incident Task whose read-only grafana-mcp agent calls propose_issue. This milestone supplies the missing pieces: the alert definitions, the routing, and the per-project enablement so that pipeline actually carries tatara alerts.
Key discovery (load-bearing)¶
Grafana in this cluster is terraform-managed (infra/terraform/grafana, grafana provider, S3 state, GitLab-CI apply), not helm/sidecar. Alert rules are simple module YAML under terraform/grafana/alerts/*.yaml; contact points + the notification-policy tree live in terraform/grafana/grafana_config.tf.
The tatara routing is prewired but broken: - grafana_contact_point.tatara ("Tatara") points at .../operator/webhooks/tatara - the SCM handler (server.go:66 r.Post("/operator/webhooks/{project}", s.handle)), not the grafana handler (server.go:67 .../{project}/grafana). A Grafana payload hits scm.Select and 400s "unrecognized provider". - The contact point sends no Authorization header, but handleGrafanaAlert (server.go:827-839) requires Grafana.Enabled (else 404) AND a constant-time Authorization: Bearer <webhookSecret> match (else 401). - The tatara Project has no spec.grafana (grafana not enabled), so the route 404s regardless. Only infrastructure is grafana-enabled (and its .critical contact-point webhook is broken the same way - it has never fired an incident end-to-end).
Consequence: near-zero tatara component-repo code. The work is terraform (alerts + contact points) + tatara-helmfile (enable + secret + metrics allowlist).
Approved decisions¶
- All Grafana-managed alerting (metric + log), one engine, routed via the existing
TataraGrafana contact point to the operator webhook. (Not per-chart PrometheusRules, not a kube-prom AlertmanagerConfig.) - Definitions live centrally in terraform (
alerts/tatara-*.yaml), covering each tatara component - not per-component chart ConfigMaps (no Grafana alerting sidecar exists; terraform is the established mechanism). - Loki/LogQL alerting included: wrapper
action=internal_issue_report(agent-reported platform problems) with the scrubbeddescriptioncarried into the alert as a label so the "what's wrong" text flows into the issue; plus a generic error-burst rule per Go component. - Severity gate:
warning+criticalalerts route to the emergency-brainstorm;infoalerts do not (they omit thesystem=tataralabel and so only reach the homelab email route, not the operator webhook). - Push-receiver allowlist fix: widen
pushMetricsAllowedPrefixesso wrapper/ingester agent-pod metrics reach Prometheus (else their metric rules are dark). The wrapper's primary signal (internal_issue_report) is a log, so it works regardless. - Fix the broken contact points for both
tataraandinfrastructure(URL +/grafanasuffix + Bearer). The SA token for grafana-mcp is reused from the existing read-onlyinfrastructure-grafanaViewer token (sharing a read-only identity is acceptable in a homelab); no new token minted.
Architecture and data flow¶
component metric (/metrics scrape) or component log (Loki ingest)
| |
v v
Prometheus (uid=prometheus) Loki (uid=<loki>, "Loki (Working)")
| |
+------------------+--------------------+
v
Grafana-managed rule (terraform alerts/tatara-*.yaml)
labels: homelab=true, system=tatara, component=<svc>, severity=warning|critical
|
v
notification policy (grafana_config.tf):
root -> policy[homelab=true] (mute weekends)
-> policy[system=tatara] -> contact_point "Tatara" (evaluated first)
-> policy[severity=critical] -> contact_point "Critical"
|
v (Bearer <webhookSecret>)
POST https://tatara.szymonrichert.pl/operator/webhooks/tatara/grafana
|
v
[live operator] handleGrafanaAlert (Enabled? bearer? firing? dedup sha256(groupKey))
v
incident Task -> agent pod (TATARA_TOOL_PROFILE=incident, grafana-mcp read-only)
v
propose_issue(repo, evidence) -> brainstorming issue -> triage/lifecycle loop
Note: the system=tatara route is a child of the homelab=true policy, so tatara alerts must carry homelab=true AND system=tatara to reach the Tatara contact point. info alerts carry homelab=true but not system=tatara, so they fall through to the homelab email route only (the severity gate).
Unit A - extend modules/grafana_alert for Loki¶
The module (terraform/grafana/modules/grafana_alert/main.tf) builds a Grafana rule from a query expression + a reduce + a round + a threshold. type_to_model currently supports only prometheus and math; a loki query_type would fail the type_to_model[query_type] lookup. Add a Loki model and let expr populate for Loki:
- Add
default_loki_model(mirrorsdefault_prometheus_model:editorMode=code,queryType="instant",instant=true,intervalMs,maxDataPoints,range=false). - Add
"loki" = local.default_loki_modeltotype_to_model. - In the runtime model
lookup, setexprforquery_typein["prometheus","loki"](currently== "prometheus"); datasourcetype/uidalready derive fromquery_typeand the per-querydatasource_uid, so a Loki query just needsdatasource_uid=<loki uid>,query_type="loki", and a LogQLexpression.
This is the only code-shaped change and stays generic (any future Loki alert benefits).
Unit B - alert inventory (terraform/grafana/alerts/tatara-*.yaml)¶
One file per group; filename (sans .yaml) becomes the rule-group name. Each rule uses the module schema: name, queries:[{expression, [datasource_uid, query_type]}], math_operator, threshold, for, decimal_points, annotations, labels. Inline comparisons from the synthesis are split into expression (the value) + math_operator + threshold (e.g. X > 0.2 -> expression X, op >, threshold 0.2; up == 0 -> expression up{...}, op <, threshold 1).
Labels on every routed rule: {homelab: "true", system: "tatara", component: "<svc>", severity: "warning"|"critical"}. info rules omit system.
B.1 Workload-generic group tatara-workloads.yaml (per component: operator, memory, chat, wrapper, ingester)¶
Rendered once per component (component label + selector substituted). kube-state-metrics / cAdvisor / up based - independent of app metrics.
| name | severity | for | expression / op / threshold |
|---|---|---|---|
Tatara<C> target down | critical (operator,memory) / warning (chat,wrapper) | 5m | up{job=~".*<svc>.*"} < 1 |
Tatara<C> crash looping | critical (operator,memory) / warning | 5m | increase(kube_pod_container_status_restarts_total{namespace="tatara",pod=~".*<svc>.*"}[15m]) > 3 |
Tatara<C> OOMKilled | critical (operator,memory) / warning | 1m | max(kube_pod_container_status_last_terminated_reason{namespace="tatara",pod=~".*<svc>.*",reason="OOMKilled"}) > 0 |
Tatara<C> container waiting | warning | 10m | sum(kube_pod_container_status_waiting_reason{namespace="tatara",pod=~".*<svc>.*",reason!=""}) > 0 |
Tatara<C> pod not ready | warning | 10m | max(kube_pod_status_ready{namespace="tatara",pod=~".*<svc>.*",condition="true"}) < 1 |
Selectors (job/pod regex) are confirmed against live Prometheus during implementation and pinned to the exact ServiceMonitor job label per component.
B.2 tatara-operator.yaml (component=operator)¶
The existing operator-chart tatara-loop PrometheusRule stays as the Prometheus-evaluated loop-health set (it routes nowhere today). To get operator alerts to the webhook under the all-Grafana-managed decision, define the operator alerts as Grafana rules here too. To avoid double-coverage confusion the synthesis-net-new operator signals are prioritized; the core loop-health alerts are reproduced as Grafana rules (single routing source for incidents).
Key rules (net-new + reproduced loop-health), all severity=warning unless noted: - Tatara operator down - up{job=~".*tatara-operator.*"} < 1 (critical, 5m). - Tatara loop wedged - sum(increase(operator_reconcile_total[15m])) < 1 (15m). - Tatara loop stalled - increase(tatara_scan_tasks_created_total[3h]) + increase(tatara_scan_items_total[3h]) < 1 (30m). - Tatara reconcile error ratio high - sum(rate(operator_reconcile_total{result="error"}[10m]))/clamp_min(sum(rate(operator_reconcile_total[10m])),0.001) > 0.2 (15m). - Tatara turn submit failure ratio high - submit error ratio > 0.3 (10m). - Tatara agent http unreachable spike - operator_agent_http_total{outcome=~"unreachable|timeout"} ratio > 0.25 (10m). - Tatara turn submit p95 high - histogram_quantile(0.95, sum(rate(operator_turn_submit_duration_seconds_bucket[10m])) by (le)) > 30 (15m). - Tatara scm write errors - sum(rate(operator_scm_writes_total{result="error"}[15m])) > 0.1 (15m). - Tatara webhook error ratio high - webhook error/bad_signature/provider_mismatch ratio > 0.2 (10m). - Tatara memory stack failed - operator_memory_stacks{phase="Failed"} > 0 (15m). - Tatara tasks inflight pinned - operator_tasks_inflight >=(> 7.999) 8 (2h). - Tatara task failures - increase(operator_task_terminal_total{phase="Failed"}[30m]) > 0. - Tatara agent boot crashloop - increase(operator_agent_boot_crash_total{outcome="failed"}[30m]) > 0. - Tatara queue depth backlog - max(operator_queue_depth) > 10 (15m).
B.3 tatara-memory.yaml (component=memory)¶
Tatara memory 5xx ratio high-http_requests_total5xx ratio (status text)>0.1(10m).Tatara memory lightrag error ratio high-lightrag_calls_total{result="error"}ratio>0.2(10m).Tatara memory lightrag p95 high-histogram_quantile(0.95, ... lightrag_call_duration_seconds_bucket ...)>10(15m).Tatara memory ingest failure ratio high-ingest_jobs_total{status="failed"}ratio>0.2(15m).Tatara memory code-graph query errors high-code_graph_query_total{result="error"}ratio>0.2(10m).Tatara memory analytics stalled-code_graph_analytics_dirty_repos > 0 and rate(code_graph_analytics_runs_total[30m]) == 0(expression returns dirty count when stalled)>0(30m).
B.4 tatara-chat.yaml (component=chat)¶
Tatara chat 5xx ratio high- chathttp_requests_total5xx ratio>0.1(10m).Tatara chat store errors high-increase(chat_store_op_errors_total[15m])>0(15m).Tatara chat store p95 high- chat store-op duration p95>threshold (15m).Tatara chat sweeper stalled- sweeper runs flat while pending>0(30m).
B.5 tatara-wrapper.yaml (component=wrapper) - requires the allowlist fix to fire¶
Tatara wrapper commit/push failing-ccw_commit_push_total{result="fail"}ratio>0.3(15m).Tatara wrapper turns erroring-ccw_turns_total{result!="success"}ratio>0.5(15m).Tatara wrapper http 5xx-ccw_http_requests_total{status_code=~"5.."}ratio>0.1(10m).Tatara wrapper http panics-increase(ccw_http_panics_total[10m])>0.Tatara wrapper metrics dark(canary, fires today) -sum(rate(operator_push_series_dropped_total{reason="reserved_name"}[10m]))>0(15m).
B.6 tatara-ingester.yaml (component=ingester) + tatara-argo.yaml (component=argo)¶
Tatara ingest job failing- kube_job_status_failed forapp.kubernetes.io/component=ingest>0(15m).Tatara ingest run failure ratio-ingest_run_result_total{result="failure"}ratio>0.25(30m).Tatara ingest job stuck active- active ingest Job age>1800s.Tatara CI workflow failing(argo) -argo_workflows_count{namespace="tatara",status="Failed"}>0(gated on the Argo controller being scraped; else dropped).
B.7 tatara-logs.yaml (Loki, query_type=loki, datasource=)¶
CRI prefix is not stripped at ingest, so each rule strips it with | pattern before | json.
Tatara agent reported platform problem(component=wrapper, severity=warning, for 0m):The scrubbedexpression: | sum by (description, category, severity) ( count_over_time( {namespace="tatara", app="tatara-claude-code-wrapper"} | pattern `<_> <_> <_> <body>` | line_format `{{.body}}` | json | action="internal_issue_report" [5m] ) ) math_operator: ">" threshold: 0 decimal_points: 0 annotations: summary: 'Agent reported a platform problem ({{ index $labels "category" }}/{{ index $labels "severity" }}): {{ index $labels "description" }}' labels: { homelab: "true", system: "tatara", component: "wrapper", severity: "warning" }descriptionrides as a label ->commonLabels-> operatorrenderAlertContext-> issue body, satisfying "include what's wrong". Cardinality is bounded (low-volume, PII-scrubbed in the wrapper tailer).Tatara <C> error log burstfor component in {operator, memory, chat} (severity=warning, for 5m):sum(count_over_time({namespace="tatara", app="tatara-<svc>"} | pattern ... | line_format ... | json | level="ERROR" [5m]))>20. Annotation links Grafana Explore (generatorURL); the incident agent fetches the offending lines live via grafana-mcp.
Unit C - labeling, routing, severity gate¶
- Every routed rule:
homelab=true(matches the homelab parent policy),system=tatara(matches the child route to the Tatara contact point),component=<svc>,severity. - The
system=tatarapolicy route already exists ingrafana_config.tf(no change needed): it is evaluated first underhomelab=trueso tatara alerts win over the critical/email route. - Severity gate = label presence:
inforules omitsystem=tatara, so they reach only the homelab email path, never the operator webhook.
Unit D - contact-point fixes (grafana_config.tf)¶
grafana_contact_point.tatara: webhookurl->https://tatara.szymonrichert.pl/operator/webhooks/tatara/grafana; addauthorization_scheme = "Bearer"andauthorization_credentials = var.tatara_webhook_secret.grafana_contact_point.criticalwebhook (-> infrastructure):url->.../operator/webhooks/infrastructure/grafana; addauthorization_scheme = "Bearer"+authorization_credentials = var.infrastructure_webhook_secret. (Scope add: makes infrastructure incidents fire; criticals already reaching this contact point will now also create incident issues.)- New sensitive variables
tatara_webhook_secret,infrastructure_webhook_secretinvariables.tf, sourced from GitLab CITF_VAR_*(never committed). Values must equal thewebhookSecretkeys in the respective*-grafanak8s Secrets the operator verifies against.
Unit E - tatara-helmfile (delivery)¶
values/project-tatara/common.yaml: addproject.spec.grafanamirroringproject-infrastructure-enabled: true,url: http://prometheus-grafana.monitoring.svc.cluster.local,secretRef: tatara-grafana.values/tatara-operator/raw/tatara-grafana.tatara-operator.pre.secrets.yaml(sops, mirror theinfrastructure-grafanaraw secret):serviceAccountToken(reuse the existing infrastructure Viewer token value) +webhookSecret(==var.tatara_webhook_secret).- Widen the operator metrics allowlist (cluster-specific override of the chart default
wrapper_,agent_,memory_,ingest_):pushMetricsAllowedPrefixes: "wrapper_,agent_,memory_,ingest_,ccw_,tatara_wrapper_,analyzer_,scip_,semantic_,llm_,push_"on the tatara-operator release values.
No infra/helmfile change (Grafana already deployed; unified alerting + prometheus/loki datasources present). No tatara component-repo code change.
Secrets and deploy prerequisites¶
tatara-grafanaSecret keys:serviceAccountToken(reused infrastructure Viewer SA token, read-only),webhookSecret(new shared value).- GitLab CI variables on the terraform repo:
TF_VAR_tatara_webhook_secret,TF_VAR_infrastructure_webhook_secretset to match the corresponding SecretwebhookSecretvalues. (infrastructure-grafana.webhookSecretalready exists in-cluster; reuse it.) - These are the only human-gated items; everything else is GitOps (terraform MR + helmfile MR).
Testing (TDD)¶
- terraform:
terraform fmt+validate+plan(CI). Module loki extension covered by a rendered-rule assertion (a Loki rule emitsdatasource.type=loki,exprpopulated,queryType=instant). A representativealerts/tatara-*.yamlasserts label set (system=tatarapresent on routed rules, absent oninfo). - Post-apply verification (verification-before-completion): rule groups
tatara-*present in Grafana (folder Default); temporarily trip one threshold (oris_pausedtoggle on a synthetic rule) and confirm atataraincidentTask + abrainstormingissue appears; confirm a syntheticinternal_issue_reportproduces an alert whosedescriptionlabel reaches the issue body. Confirm wrapperccw_*series now present in Prometheus after the allowlist change (operator_push_series_dropped_total{reason="reserved_name"}stops rising).
Deploy order¶
- tatara-helmfile MR: enable GrafanaSpec on the tatara Project +
tatara-grafanasops secret + allowlist widen. Apply ->grafana-mcp-tataraprovisioned, route accepts. - terraform MR: module Loki extension +
alerts/tatara-*.yaml+ contact-point fixes (tatara + infrastructure) + sensitive vars (CI vars set first). CI plan reviewed -> apply -> rules live, routing works. - Verify end-to-end per Testing.
Either MR is safe to land first: until the Project is grafana-enabled the webhook 404s (alerts fire to email only); until the rules exist nothing routes. No breakage from ordering.
Out of scope¶
- Remediation/write actions (grafana-mcp stays
--disable-write; incident agent files an issue only) - per the 2026-06-19 design. - Per-component chart ConfigMaps / a Grafana alerting sidecar (rejected: terraform is the established mechanism).
- Reworking the homelab notification tree beyond adding the (already-present) tatara route and fixing the two contact points.
- HMAC webhook auth (bearer is the baseline).
Risks / notes¶
- Selector accuracy:
job/pod/applabel values must be pinned against live Prometheus/Loki during implementation (operator/memory/chat/wrapper confirmed asapp=tatara-<svc>in Loki). - Fixing the
infrastructurecontact point changes live behavior: homelab criticals routed to that contact point will now create incident issues. Accepted by the user. - Loki
description-as-label cardinality is bounded by lowinternal_issue_reportvolume and wrapper-side PII scrubbing; revisit if volume grows. - Known redundancy (rule 4, documented not deferred-silently): once operator loop-health is reproduced as Grafana rules, the operator-chart
tatara-loopPrometheusRule is dead weight - it still evaluates in Prometheus but routes to the kube-prom Alertmanagernullreceiver, so it cannot double-fire an incident (only Grafana reaches the webhook). It is retained this milestone to honor "no tatara component-repo change"; the clean-up (remove the PrometheusRule template from the operator chart) is a small follow-up operator-chart change, tracked in the operator ROADMAP. Until then the duplication is inert.