Monitoring and Observability¶
MANA ships a complete observability setup (Prometheus, Grafana, Loki, Alertmanager) and instruments both the orchestrator and the sample app. This guide explains what is measured, how to install and access the stack, which dashboards and alerts exist, and where the current gaps are.
What Is Observable¶
| Signal | Source | Where it goes |
|---|---|---|
| Orchestrator metrics | GET /metrics/ on port 8000 |
Prometheus |
| Sample app metrics | GET /metrics on port 5000 |
Prometheus |
| Kubernetes metrics | kube-state-metrics, cAdvisor, node exporter | Prometheus |
| Audit events and application logs | stdout of each pod | Promtail to Loki |
| Health | GET /health on the orchestrator and the app |
Kubernetes probes, Prometheus up |
Configuration files are in monitoring/ (see Components).
Prometheus Metrics¶
Orchestrator¶
| Metric | Type | Labels | Meaning | Populated |
|---|---|---|---|---|
deployments_total |
counter | namespace, status |
Webhooks received, by status (in_progress, success, failed) |
yes |
retries_total |
counter | namespace, retry_attempt |
Retries scheduled | yes |
rollbacks_total |
counter | namespace, reason |
Successful rollbacks (reason is the failure type, or manual) |
yes |
health_check_duration_seconds |
histogram | namespace, deployment |
Duration of POST /health-check calls |
yes |
deployment_duration_seconds |
histogram | namespace |
Intended deployment duration | declared, never observed |
active_deployments |
gauge | namespace |
Intended count of in-flight deployments | declared, never set |
Standard python_* and process_* metrics are also exported. Use curl -s $MANA/metrics/ to see the live output.
Sample app¶
Provided by prometheus-flask-exporter at GET /metrics:
| Metric | Meaning |
|---|---|
flask_http_request_total{method,status} |
Request count |
flask_http_request_duration_seconds |
Latency histogram |
flask_http_request_exceptions_total |
Unhandled exceptions |
app_info{version} |
Application version |
Useful queries¶
# Deployment events by status over the last hour
sum(increase(deployments_total[1h])) by (status)
# Failure ratio per namespace (5 min)
sum(rate(deployments_total{status="failed"}[5m])) by (namespace)
/ sum(rate(deployments_total[5m])) by (namespace)
# Rollbacks in the last 24 h, by reason
sum(increase(rollbacks_total[24h])) by (reason)
# Retries by attempt number
sum(increase(retries_total[1h])) by (retry_attempt)
# Sample app 5xx error ratio
sum(rate(flask_http_request_total{status=~"5.."}[5m]))
/ sum(rate(flask_http_request_total[5m]))
# Sample app p95 latency
histogram_quantile(0.95, sum(rate(flask_http_request_duration_seconds_bucket[5m])) by (le))
# Pod restarts
sum(rate(kube_pod_container_status_restarts_total{namespace="production"}[15m])) by (pod)
Recording rules¶
monitoring/prometheus/recording-rules.yml pre-computes success, failure, rollback and retry rates (deployment:success_rate:5m, deployment:rollback_rate:1h, and so on), HTTP rates and latency percentiles (http:*), node and pod resource usage, SLO and error-budget series (slo:*, mttr:rollback_duration:avg:24h), and some orchestrator series (orchestrator:webhook_error_rate:5m, orchestrator:rollback_decision_seconds:p95:5m). Rules that depend on metrics the orchestrator does not emit (webhook, rollback decision and deployment duration series) produce no data yet.
How Prometheus finds the targets¶
| Deployment | Mechanism |
|---|---|
Terraform (kube-prometheus-stack) |
ServiceMonitors created by the monitoring module, plus pod annotations on the orchestrator (prometheus.io/scrape, port: 8000, path: /metrics) |
| Sample app manifests | Pod annotations prometheus.io/scrape: "true", port: "5000", path: "/metrics" |
Standalone prometheus.yml (Compose) |
Dedicated sample-app and orchestrator jobs plus annotation-based pod discovery |
!!! warning "Target configuration to double-check" - The manual orchestrator manifest (orchestrator/k8s/deployment.yaml) has no prometheus.io/* annotations, so annotation-based discovery will not find it; add them to the pod template or use the Terraform module. - In prometheus.yml the sample-app and orchestrator jobs set __address__ to the literal text __meta_kubernetes_pod_ip:<port> instead of building it from the label with source_labels and a ${1} replacement, so those two jobs likely will not resolve a valid address. Verify on the Prometheus Targets page and fix the relabeling if they show errors. - The Compose Prometheus loads alerts.yml but does not mount recording-rules.yml, and expects an Alertmanager at alertmanager:9093 that Compose does not run.
Structured Logging¶
Audit events¶
Every significant orchestrator action produces an audit event (deployment_received, failure_detected, retry_initiated, rollback_initiated, rollback_completed, rollback_failed, rollback_skipped, health_check_*, state_saved, manual_intervention_required). Each carries a UTC timestamp, deployment ID, namespace, version and a success flag, plus action-specific details.
kubectl -n orchestrator logs -l app=orchestrator | grep "AUDIT:"
The orchestrator's default log format prints AUDIT: <action> and attaches the structured payload to the record as audit_event, which the plain text formatter does not print. To ship the complete JSON to Loki, configure a JSON formatter (for example python-json-logger, already used by the sample app) that includes extra fields. Until then, correlate by deployment_id in the surrounding log lines.
Sample app logs¶
With ENVIRONMENT=production the sample app logs JSON with a per-request correlation ID (X-Request-ID); otherwise plain text.
Querying logs in Loki¶
Promtail labels logs with namespace, pod, container, app and version (and level/logger when the line is JSON). In Grafana, Explore, choose the Loki data source:
{namespace="orchestrator", app="orchestrator"}
{namespace="orchestrator"} |= "AUDIT:"
{namespace="orchestrator"} |= "manual_intervention_required"
{namespace="production", app="sample-app"} | json | level="ERROR"
sum(rate({namespace="production", app="sample-app"} | json | level="ERROR" [5m]))
Loki retains logs for 30 days (720h) in the shipped config.
Health Endpoints¶
| Service | Endpoint | Meaning |
|---|---|---|
| Orchestrator | GET /health |
healthy or degraded; reports Redis, Kubernetes and GitHub integration |
| Sample app | GET /health |
Liveness |
| Sample app | GET /ready |
Readiness (503 until STARTUP_TIME passes) |
The orchestrator also exposes GET /config to confirm its live settings.
Dashboards¶
Four Grafana dashboards live in monitoring/grafana/dashboards/:
| Dashboard | Shows |
|---|---|
| Self-Healing Overview | Success rate, rollback and retry activity, failed deployments, MTTR and error budget, orchestrator health, pod restarts, recent logs |
| Deployment Metrics | Per-namespace deployment and rollback statistics, durations, SLO and error budget |
| Application Metrics | Request rate, error rate, latency percentiles, resource usage of the sample app |
| Infrastructure | Node CPU and memory, cluster capacity, quotas |
Several panels use the recording rules above. Panels built on active_deployments, deployment duration and orchestrator rollback-decision series are empty until those metrics are emitted.
Access¶
make port-forward-grafana # http://localhost:3000
make port-forward-prometheus # http://localhost:9090
Default Grafana login is admin / admin123. Change it before exposing Grafana anywhere (Security).
Dashboard provisioning
Terraform loads the dashboard from terraform/modules/monitoring/dashboards/self-healing.json into a ConfigMap. The Compose setup mounts monitoring/grafana/dashboards at /var/lib/grafana/dashboards, but dashboards.yml expects the files under /etc/grafana/dashboards/{self-healing,deployments,application,infrastructure}. If Grafana in Compose shows no dashboards, align those paths or import the JSON files manually (Dashboards, New, Import).
Alerts¶
Alert rules are in monitoring/prometheus/alerts.yml and are routed by Alertmanager (monitoring/alertmanager/alertmanager.yml):
| Severity | Receivers |
|---|---|
critical |
PagerDuty and Slack (orchestrator, application and infrastructure criticals have dedicated receivers) |
warning |
Slack |
| SLO violations | Slack and email |
Highlights (full table with thresholds in the Configuration Reference):
- Deployments:
HighRollbackRate,DeploymentFailureRate,DeploymentSuccessRateBelowSLO,DeploymentReplicasMismatch,PodCrashLooping - Orchestrator:
OrchestratorDown,SlowRollbackDecision,HighWebhookFailureRate,RedisConnectionFailures - Application:
HighErrorRate,SlowResponseTime,ApplicationDown, memory and CPU - Infrastructure and monitoring: node readiness, disk, scrape failures, Grafana/Loki down
Five rules (SlowRollbackDecision, HighWebhookFailureRate, RedisConnectionFailures, HighHealthCheckFailureRate, SlowDeployment) query metrics the orchestrator does not emit yet and will stay silent.
Before enabling notifications, replace the placeholders in alertmanager.yml (sender address, SMTP host) and mount the Slack webhook URL, SMTP password and PagerDuty keys from Kubernetes Secrets (/etc/alertmanager/slack_webhook_url, smtp_password).
Test an alert safely:
kubectl -n production scale deployment sample-app --replicas=0
# wait for ApplicationDown (2 min) / DeploymentReplicasMismatch (5 min)
make port-forward-prometheus # http://localhost:9090/alerts
kubectl -n production scale deployment sample-app --replicas=3
Setting Up the Stack¶
cd terraform
terraform apply -var="enable_monitoring=true"
kubectl get pods -n monitoring
Installs kube-prometheus-stack (Prometheus, Grafana, Alertmanager, kube-state-metrics, node exporter) and loki-stack, with 7-day, 10 GB Prometheus retention.
make dev-start
# Grafana http://localhost:3000 Prometheus http://localhost:9090 Loki http://localhost:3100
See the caveats above and in Development Setup.
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus prometheus-community/kube-prometheus-stack \
-n monitoring --create-namespace --set prometheus.prometheusSpec.retention=7d
Then apply the rules and dashboards from monitoring/.
Verify¶
# Targets
curl -s localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health: .health}'
# Orchestrator metric present?
curl -s 'localhost:9090/api/v1/query?query=deployments_total' | jq '.data.result | length'
# Generate data
curl -s -X POST $MANA/webhook/deployment -H 'Content-Type: application/json' \
-d '{"deployment_id":"mon-1","namespace":"staging","app_name":"sample-app","version":"1","status":"success"}'
Best Practices¶
- Alert on outcomes, not only components. Success rate, rollback rate, and
manual_intervention_requiredtell you if healing works. - Watch MANA itself:
up{job="orchestrator"}, pod restarts, and/healthdegradation. - Fix the empty-metric gaps (webhook counters, rollback decision latency, deployment duration) so the shipped alerts and panels become live.
- Keep cardinality in check.
retries_totalhas aretry_attemptlabel bounded byMAX_RETRIES; avoid adding deployment IDs as labels. - Retention: 7 days of metrics and 30 days of logs by default; raise them if you need longer incident history.
- Secure access to Grafana and Prometheus, and keep dashboards in Git rather than editing them only in the UI.
Troubleshooting Monitoring¶
| Symptom | Check |
|---|---|
Orchestrator target missing or DOWN |
Pod annotations (manual install) or ServiceMonitor (Terraform); relabeling of __address__; port 8000 reachable; use /metrics/ (a bare /metrics redirects) |
| Metrics exist but panels are empty | The panel uses a recording rule or metric that is not emitted yet; run the underlying query in Prometheus |
| No dashboards in Grafana | Provisioning paths (see warning above) or import manually |
| No logs in Loki | Promtail running? Check kubectl logs -n monitoring -l app=promtail; query {namespace="orchestrator"} first |
| Alerts never fire | Rule loaded (/rules page)? Metric present? for: duration elapsed? |
| Alerts fire but nothing arrives | Alertmanager receivers and secrets; check /#/status on port 9093 |
| Prometheus out of disk | Reduce retention or raise the volume size (10Gi by default) |