Bonnes pratiques
Haute disponibilité (HA)
Prometheus / Mimir
| Composant | Stratégie HA | Configuration |
| Prometheus | 2 replicas identiques (scraping parallele) | --storage.tsdb.retention.time=7d sur chaque replica |
| Mimir Ingester | 3 replicas minimum, réplication factor 3 | Zone-aware réplication si multi-AZ |
| Mimir Querier | 2+ replicas (stateless) | Load balancer devant les queriers |
| Mimir Store Gateway | 2+ replicas | Sharding par bloc |
# Mimir — configuration HA
ingester:
ring:
replication_factor: 3
zone_awareness_enabled: true
kvstore:
store: etcd
Loki
| Mode | Quand l'utiliser | HA |
| Monolithique | < 50 Go/jour de logs | Non (single point of failure) |
| Simple Scalable | 50-500 Go/jour | Oui (3 replicas read + 3 write) |
| Micro-services | > 500 Go/jour | Oui (composants indépendants) |
# Loki Simple Scalable — 3 replicas par tier
write:
replicas: 3
persistence:
size: 50Gi
read:
replicas: 3
backend:
replicas: 2
Grafana
# Grafana HA avec base de donnees partagee
replicas: 2
database:
type: postgres
host: postgresql.observability:5432
name: grafana
user: grafana
password: ${GRAFANA_DB_PASSWORD}
# Partage des sessions entre replicas
session:
provider: redis
provider_config: addr=redis.observability:6379
Alerting
Principes fondamentaux
| Principe | Description |
| Actionnable | Chaque alerte doit correspondre a une action concrète |
| Runbook | Chaque alerte a un lien vers une procedure de résolution |
| Sévérité claire | Critical = intervention immédiate, Warning = investigation sous 4h |
| Routage | Les alertes arrivent a la bonne équipe (pas de broadcast) |
| Anti-fatigue | Grouper, dedupliquer, silencer pendant les maintenances |
Structure d'une regle d'alerte
# Regle d'alerte Prometheus / Alertmanager
groups:
- name: service-slo
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
> 0.01
for: 5m
labels:
severity: critical
team: platform
annotations:
summary: "Taux d'erreur > 1% pour {{ $labels.service }}"
description: "Le service {{ $labels.service }} a un taux d'erreur de {{ $value | humanizePercentage }} sur les 5 dernieres minutes."
runbook_url: "https://wiki.internal/runbooks/high-error-rate"
dashboard_url: "https://grafana.internal/d/service-overview?var-service={{ $labels.service }}"
Routage Alertmanager
# alertmanager.yml
route:
group_by: ['alertname', 'service']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: default
routes:
# Alertes critiques → PagerDuty + Slack
- match:
severity: critical
receiver: pagerduty-critical
continue: true
- match:
severity: critical
receiver: slack-critical
# Alertes warning → Slack uniquement
- match:
severity: warning
receiver: slack-warning
# Alertes securite → equipe securite
- match:
team: security
receiver: slack-security
receivers:
- name: default
slack_configs:
- channel: '#alerts-default'
- name: pagerduty-critical
pagerduty_configs:
- routing_key: ${PAGERDUTY_KEY}
- name: slack-critical
slack_configs:
- channel: '#alerts-critical'
send_resolved: true
- name: slack-warning
slack_configs:
- channel: '#alerts-warning'
send_resolved: true
- name: slack-security
slack_configs:
- channel: '#security-alerts'
send_resolved: true
Lutter contre la fatigue d'alerte
| Technique | Description |
| Groupage | Regrouper les alertes similaires (group_by) |
| Deduplication | Alertmanager deduplique automatiquement |
| Silences | Silencer pendant les maintenances planifiees |
| Inhibition | Si le cluster est down, ne pas alerter sur chaque service |
| Burn rate | Alerter sur la vitesse de consommation de l'error budget, pas sur des seuils fixes |
| Revue régulière | Supprimer les alertes jamais actionnees (review trimestrielle) |
Burn rate alerting
Au lieu d'alerter sur "taux d'erreur > 1%", alerter sur "au rythme actuel, le SLO sera depasse dans X heures". Cela reduit les faux positifs et donne du contexte.
# Burn rate : consommation de l'error budget sur 1h vs 30 jours
(
1 - (sum(rate(http_requests_total{status!~"5.."}[1h])) / sum(rate(http_requests_total[1h])))
)
/
(1 - 0.999) # SLO 99.9%
> 14.4 # Burn rate critique (budget consomme en ~1h)
Design de dashboards
Golden signals par service
Chaque service doit avoir un dashboard avec 4 panels principaux :
graph TD
subgraph Dashboard["Service: service_name"]
subgraph Row1[" "]
Rate["Request Rate (req/s)"]
Error["Error Rate (%)"]
end
subgraph Row2[" "]
Latency["Latency p50/p95/p99"]
Saturation["Saturation (CPU/Mem)"]
end
Logs["Recent Logs (errors + warnings)"]
Traces["Active Traces (erreurs) — Lien vers Tempo"]
end
Conventions de nommage
| Élément | Convention | Exemple |
| Dashboard | [Zone] Service - Vue | [Production] Keycloak - Overview |
| Variable | $service, $namespace, $instance | Template variables Grafana |
| Panel titre | Action + objet | "Taux de requêtes", "Latence p99" |
| Unites | Toujours specifier l'unite | req/s, ms, %, octets |
| Seuils | Rouge > critique, Orange > warning | Coherent avec les alertes |
Anti-patterns dashboards
A eviter
- Dashboard mural : > 20 panels sur un seul dashboard (illisible)
- Metriques sans contexte : un graphe sans titre, sans unite, sans seuil
- Dashboards manuels : crees dans l'interface sans etre versionnes en Git
- Copier-coller : dupliquer un dashboard au lieu d'utiliser des variables
- Tout sur un seul dashboard : séparer infrastructure, application, business
Maîtrise des coûts
Cardinalite des metriques
La cardinalite (nombre de combinaisons uniques de labels) est le premier facteur de coût :
| Action | Impact |
| Supprimer les labels inutiles | Reduire la cardinalite de 10-50% |
| Eviter les labels a valeurs infinies (request_id, user_id) | Eviter l'explosion des series |
Utiliser metric_relabel_configs pour filtrer | Reduire a la source |
| Aggreger les metriques internes non nécessaires | Moins de series stockees |
# Prometheus — filtrer les metriques inutiles a la source
scrape_configs:
- job_name: "keycloak"
metric_relabel_configs:
# Garder uniquement les metriques utiles
- source_labels: [__name__]
regex: "keycloak_(logins|login_errors|registrations|request_duration|active_sessions).*"
action: keep
# Supprimer les labels a haute cardinalite
- regex: "instance"
action: labeldrop
Volume de logs
| Action | Impact |
| Filtrer les logs DEBUG en production | Reduire le volume de 50-80% |
| Echantillonner les logs d'accès normaux | Garder 10% du trafic normal, 100% des erreurs |
| Compresser les logs (Loki compresse nativement) | ~10x de compression |
| Retention differenciee (voir chapitre Confidentialite) | Reduire le stockage longue duree |
// Alloy — filtrer les logs DEBUG avant envoi a Loki
loki.process "drop_debug" {
stage.match {
selector = '{level="debug"}'
action = "drop"
}
forward_to = [loki.write.default.receiver]
}
Échantillonnage des traces
// Alloy — tail-based sampling
// Garder 100% des traces en erreur, 10% des traces normales
otelcol.processor.tail_sampling "default" {
decision_wait = 10s
policy {
name = "errors"
type = "status_code"
status_code {
status_codes = ["ERROR"]
}
}
policy {
name = "slow"
type = "latency"
latency {
threshold_ms = 1000
}
}
policy {
name = "default"
type = "probabilistic"
probabilistic {
sampling_percentage = 10
}
}
output {
traces = [otelcol.exporter.otlp.tempo.input]
}
}
Troubleshooting
Metriques manquantes
| Symptome | Cause probable | Diagnostic |
| Metrique absente dans Grafana | Target non scrape | Vérifier http://prometheus:9090/targets |
| Metrique presente puis disparait | Pod redeploy, label change | Vérifier les relabel configs |
| Valeurs a 0 | Compteur reinitialise (restart) | Utiliser rate() ou increase() |
| "No data" dans un panel | Mauvaise requête ou mauvaise datasource | Tester dans Prometheus UI d'abord |
# Verifier qu'un endpoint expose des metriques
curl -s http://service:8080/metrics | head -20
# Verifier les targets Prometheus
curl -s http://prometheus:9090/api/v1/targets | \
jq '.data.activeTargets[] | {job: .labels.job, health: .health, lastError: .lastError}'
# Verifier la cardinalite
curl -s http://prometheus:9090/api/v1/status/tsdb | \
jq '.data.seriesCountByMetricName[:10]'
| Symptome | Cause probable | Solution |
| Requête LogQL lente (> 30s) | Scan sur trop de données | Ajouter des labels pour restreindre, reduire la fenêtre temporelle |
| "context deadline exceeded" | Timeout de requête | Augmenter limits_config.max_query_length |
| Ingestion rejetee (429) | Rate limit atteint | Augmenter limits_config.ingestion_rate_mb |
| Labels manquants | Pipeline Alloy mal configure | Vérifier les stages labels dans la config Alloy |
# Bonne pratique : toujours commencer par un filtre de labels
# BIEN : filtre label d'abord, puis contenu
{service="catalog", level="error"} |= "timeout"
# MAL : scan de tous les logs puis filtre
{job=~".+"} |= "timeout"
Permissions Grafana
| Symptome | Cause | Solution |
| "Access denied" sur un dashboard | Rôle insuffisant | Vérifier les permissions du dossier Grafana |
| Datasource invisible | Pas de permission sur la datasource | Ajouter le rôle dans les datasource permissions |
| Alerte non routee | Mauvais contact point | Vérifier la configuration Alertmanager / Grafana alerting |
| Dashboard vide apres login SSO | Mauvais mapping de rôles OIDC → Grafana | Vérifier role_attribute_path dans la config OIDC |
# Verifier les permissions d'un utilisateur
curl -s http://grafana:3000/api/user/orgs \
-H "Authorization: Bearer ${USER_TOKEN}" | jq .
# Lister les permissions d'un dossier
curl -s http://grafana:3000/api/folders/${FOLDER_UID}/permissions \
-H "Authorization: Bearer ${ADMIN_TOKEN}" | jq .