Publicat în

Îmbogățirea Alertelor Alertmanager cu Sidecar de Enrichment: Context Imediat pentru On-Call Engineers

Fiecare inginer de on-call cunoaște ritualul: o alertă aterizează în Slack sau PagerDuty — „rate 5xx > 5% pe serviciul payments” — și mesajul îți spune ce s-a întâmplat, dar nu de ce. Deschizi Grafana Loki, setezi fereastra de timp, filtrezi după service și namespace, cauți erori în loguri, apoi revii la alertă cu contextul găsit. Această buclă manuală consumă minute prețioase în momente critice. Soluția? Un sidecar de enrichment care atașează automat dovezile (loguri relevante, metrici, trace-uri) direct la notificarea Alertmanager, înainte ca aceasta să ajungă la receiver. În acest articol explorăm arhitectura, implementarea și avantajele practice ale unui astfel de sidecar, cu exemple concrete pentru medii de hosting și infrastructură dedicată.

Arhitectura Sidecar-ului de Enrichment: Principii și Flux de Date

Un sidecar de enrichment funcționează ca un proxy inteligent între Alertmanager și receiverele finale (Slack, PagerDuty, Opsgenie, webhook-uri custom). Alertmanager este configurat să trimită alertele către sidecar (de obicei pe localhost:9094 sau un service Kubernetes dedicat), sidecarul îmbogățeste payload-ul cu date din surse externe — Loki, Elasticsearch, Tempo, Prometheus — și apoi forward-uiește alerta îmbogățită către receiver-ul real.

Fluxul simplificat:

Prometheus → Alertmanager → Enrichment Sidecar → Receiver (Slack/PagerDuty/etc.)
                    ↓
           Interceptare alertă
                    ↓
           Query surse de date (Loki, Tempo, etc.)
                    ↓
           Așezare evidence în annotations/links
                    ↓
           Forward la receiver final

Avantajul cheie: Alertmanager rămâne neschimbat — continua să gestioneze grouping, inhibition, silencing și deduplicare. Sidecarul este stateless, horizontal scalabil și poate fi deployat ca Deployment Kubernetes sau container sidecar în același pod cu Alertmanager (deși separarea oferă izolare mai bună la crash-uri).

Pentru medii de hosting gestionate, unde platforma dedicated-servers.ro oferă servere dedicate cu acces root complet, acest pattern permite clienților să implementeze enrichment fără a modifica configurarea Alertmanager gestionată de provider.

Implementare Practică: Sidecar Go cu Loki și Tempo Integration

Mai jos este o implementare minimală în Go care demonstrează conceptul. Sidecarul expune un endpoint HTTP /enrich care primește payload-ul Alertmanager (format AlertmanagerWebhook), interoghează Loki pentru loguri de eroare din ultimele 5 minute pentru label-urile alertei, și adaugă rezultatele ca annotation evidence_logs și un link către Grafana Explore.

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/prometheus/alertmanager/template"
)

type EnrichmentConfig struct {
    LokiURL      string `env:"LOKI_URL"      envDefault:"http://loki:3100"`
    TempoURL     string `env:"TEMPO_URL"     envDefault:"http://tempo:3200"`
    GrafanaURL   string `env:"GRAFANA_URL"   envDefault:"http://grafana:3000"`
    LookbackWindow time.Duration `env:"LOOKBACK" envDefault:"5m"`
}

func main() {
    cfg := EnrichmentConfig{}
    // parse env vars aici (omise pentru breviate)

    http.HandleFunc("/enrich", func(w http.ResponseWriter, r *http.Request) {
        var data template.Data
        if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
            http.Error(w, "invalid payload", http.StatusBadRequest)
            return
        }

        enrichedAlerts := make([]template.Alert, 0, len(data.Alerts))
        for _, alert := range data.Alerts {
            enriched := enrichAlert(alert, cfg)
            enrichedAlerts = append(enrichedAlerts, enriched)
        }

        data.Alerts = enrichedAlerts
        // Forward la receiver real
        forwardToReceiver(data, cfg)
        w.WriteHeader(http.StatusOK)
    })

    log.Fatal(http.ListenAndServe(":9094", nil))
}

func enrichAlert(alert template.Alert, cfg EnrichmentConfig) template.Alert {
    // Construiește query LogQL bazat pe label-uri
    labels := alert.Labels
    service := labels["service"]
    namespace := labels["namespace"]
    pod := labels["pod"]

    logql := fmt.Sprintf(`{service="%s", namespace="%s"} |= "error" |= "exception" | json`, service, namespace)
    if pod != "" {
        logql = fmt.Sprintf(`{service="%s", namespace="%s", pod="%s"} |= "error" | json`, service, namespace, pod)
    }

    // Query Loki
    logs, err := queryLoki(cfg.LokiURL, logql, cfg.LookbackWindow)
    if err != nil {
        log.Printf("Loki query failed: %v", err)
    } else if len(logs) > 0 {
        // Limitează la 10 linii cele mai recente
        if len(logs) > 10 {
            logs = logs[:10]
        }
        alert.Annotations["evidence_logs"] = truncateLogs(logs, 4000)
    }

    // Link Grafana Explore pre-completat
    exploreURL := buildGrafanaExploreURL(cfg.GrafanaURL, logql, cfg.LookbackWindow)
    alert.GeneratorURL = exploreURL

    return alert
}

func queryLoki(lokiURL, logql string, lookback time.Duration) ([]string, error) {
    end := time.Now()
    start := end.Add(-lookback)
    url := fmt.Sprintf("%s/loki/api/v1/query_range?query=%s&start=%d&end=%d&limit=100",
        lokiURL, url.QueryEscape(logql), start.UnixNano(), end.UnixNano())

    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    // Parse JSON response, extragere linii log
    // Implementare completă necesită parsing structurat
    return []string{string(body)}, nil // placeholder
}

Configurarea Alertmanager pentru a folosi sidecarul:

# alertmanager.yml
global:
  resolve_timeout: 5m

route:
  group_by: ['alertname', 'service', 'namespace']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'enrichment-sidecar'

receivers:
- name: 'enrichment-sidecar'
  webhook_configs:
  - url: 'http://enrichment-sidecar:9094/enrich'
    send_resolved: true

# Receiverul final (Slack) este configurat în sidecar, nu aici

Această abordare funcționează excelent pe serverele dedicate de la serverspan.ro unde avem control complet pe stack-ul de monitoring și putem deploya sidecar-ul ca systemd service sau container Podman/Docker.

Strategii de Query Inteligent: Reducerea zgomotului și Costurilor

Interogarea naivă a Loki pentru fiecare alertă generează costuri și latente. Trei optimizări esențiale:

1. Cache la nivel de sidecar — Stochează rezultatele query-urilor recente (key: hash de label-uri + fereastră temporală) pentru 2-3 minute. Majoritatea alertelor din același grup (ex: 5xx rate pe același serviciu) vor primi același context.

2. Sample-based enrichment — Pentru alerte de tip „high cardinality” (multe instanțe), enrichiează doar o singură alertă reprezentativă per grup. Alertmanager grupă deja alertele; sidecarul poate detecta alert.Labels["__alert_group_key"] și să execute query o singură dată.

3. Tiered evidence — Nivel 1 (instant): metrici Prometheus relevante (ex: rate(http_requests_total{status=~"5.."}[1m]) pentru serviciul alertat). Nivel 2 (sub 2s): loguri Loki filtrate pe level="error". Nivel 3 (opțional, async): trace-uri Tempo pentru request-id-uri din loguri.

Exemplu configurație sidecar pentru tiered evidence:

# enrichment-sidecar-config.yml
enrichment:
  tiers:
    - name: "metrics"
      enabled: true
      queries:
        - promql: 'rate(http_requests_total{service="{{.Labels.service}}",status=~"5.."}[1m])'
          annotation: "evidence_5xx_rate"
    - name: "logs"
      enabled: true
      loki:
        logql_template: '{service="{{.Labels.service}}", namespace="{{.Labels.namespace}}"} |~ "(?i)(error|exception|panic|fatal)"'
        lookback: "5m"
        max_lines: 15
        annotation: "evidence_logs"
    - name: "traces"
      enabled: false  # activare selectivă
      tempo:
        traceql: 'span.service.name = "{{.Labels.service}}" && span.status = "error"'
        lookback: "10m"
        max_traces: 3
        annotation: "evidence_traces"

Securitate, Observabilitate și Operabilitate în Producție

Un sidecar de enrichment devine componentă critică în lanțul de alertă — dacă cade, alertele nu ajung la on-call. Considerați următoarele:

High Availability — Deployați minimum 2 replici cu un Service Kubernetes enrichment-sidecar care face load balancing. Alertmanager trebuie configurat cu webhook_configs multiple sau un singur URL care pointează către Service (nu pod direct).

Timeout-uri și Circuit Breaker — Setați timeout-uri aggressive pe query-uri externe (Loki: 2s, Tempo: 3s, Prometheus: 1s). Dacă o sursă nu răspunde, sidecarul trebuie să continue cu ce are și să adauge annotation evidence_partial: "loki_timeout". Implementați circuit breaker (ex: go-breaker) pentru a opri query-urile către o sursă cronic lentă.

Autentificare și Autorizare — Sidecarul are nevoie de credentiale pentru Loki/Tempo/Prometheus. Folosiți Kubernetes Secrets montate ca fișiere sau variabile de mediu. Pentru medii multi-tenant (hosting partajat), fromționați sidecar-ul per tenant cu RBAC distinct.

Observabilitate a sidecar-ului — Expuneți metrics Prometheus proprii:

  • enrichment_requests_total{status="success|timeout|error"}
  • enrichment_latency_seconds{bucket="..."}
  • enrichment_evidence_attached_total{type="logs|metrics|traces"}
  • enrichment_cache_hits_total / enrichment_cache_misses_total

Alertați pe enrichment_requests_total{status="error"} > 0.05 pentru 5 minute.

Testing în staging — Simulați alerte cu amtool și verificați payload-ul final:

amtool alert add test_alert service=payments namespace=prod severity=critical \
  --annotation=summary="Test enrichment" \
  --alertmanager.url=http://alertmanager:9093

Verificați în logurile sidecar-ului că query-urile Loki/Tempo sunt executate și că annotation-urile sunt populate corect.

Pași Practici pentru Implementare Rapidă

  1. Deploy sidecar ca Deployment Kubernetes cu 2 replici, resource limits (CPU 200m, RAM 256Mi), liveness/readiness probes pe /health
  2. Configurează Alertmanager să trimită alerte către http://enrichment-sidecar.monitoring.svc:9094/enrich
  3. Adaugă Secret-uri pentru token-uri Loki/Tempo/Prometheus/Grafana
  4. Activează tiered evidence în config: metrics (instant) + logs (5m lookback) ca default
  5. Setează alertă pe sidecar pentru failure rate > 5% și latencies p99 > 3s
  6. Testează end-to-end cu amtool și verifică în Slack/PagerDuty că evidence_logs și link-urile Grafana apar
  7. Documentează runbook-ul pentru on-call: cum să citească evidence, cum să dezactiveze enrichment temporar (kubectl scale deployment enrichment-sidecar --replicas=0)

Concluzie

Un sidecar de enrichment pentru Alertmanager transformă alertele din simple notificări „ceva e stricat” în dosare complete de investigație: metrici relevante, loguri de eroare, link-uri directe în Grafana Explore, opțional trace-uri distribuite. Arhitectura decoupled păstrează Alertmanager curat și concentrat pe routing, während sidecarul gestionează complexitatea de query și formatare. Pentru echipele care rulează infrastructură critică pe servere dedicate sau cloud privat, acest pattern reduce MTTR (Mean Time To Resolution) cu minute la fiecare incident — diferența între o rezolvare în 5 minute și una în 30. Implementarea nu necesită modificări la Prometheus sau Alertmanager, doar un componentă suplimentară bine testată și monitorizată. Începeți cu tier-ul de metrics (instant, zero cost) și adăugați loguri și trace-uri incremental, măsurând impactul pe noise-reduction și timp de investigație.

Lasă un răspuns

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *