Prometheus de A à Y
Qu’est-ce que Prometheus ?
Prometheus est une solution de supervision créée par SoundCloud en 2012 et open-sourcée en 2015. En 2016, Prometheus est le deuxième projet ayant rejoint la Cloud Native Computing Foundation (le premier étant Kubernetes).
Prometheus est conçu pour monitorer des métriques provenant d’applications ou de serveurs.
Il se décompose en 3 parties :
- le requêteur pour récupérer les métriques sur les exporters;
- la Time Series Database (TSDB) stockant les données à court-terme;
- le service web permettant de requêter la base de donnée;
Il intègre également un outil d’alerting réagissant lorsqu’une métrique dépasse un seuil considéré comme critique.
Cette stack n’utilise pas d’agents à proprement parler, Prometheus se base sur des exporters : des micro-services se chargeant de récupérer des informations sur l’hôte et de les exposer sur une page web dans un format propre à Prometheus.
Exemple:
# HELP node_disk_written_bytes_total The total number of bytes written successfully.
# TYPE node_disk_written_bytes_total counter
node_disk_written_bytes_total{device="sda"} 1.71447477248e+11
node_disk_written_bytes_total{device="sr0"} 0
Exemple de cas d’usage : Pour monitorer un système Linux ainsi que les conteneurs Docker présents sur celui-ci, il est nécessaire d’installer un premier exporter pour les métriques systèmes et un second pour le daemon Docker. Un (ou plus) serveur Prometheus pourra alors requêter ces deux exporters pour récupérer les données.
Prometheus se charge de récupérer les données en interrogeant les exporters lui-même, il fonctionne en Pull et non en Push !
La base de donnée de Prometheus possède un Index basé sur le temps (Comme InfluxDB) mais également sur des ensembles de couples clé-valeur : des labels. Les labels définissent le contexte d’une information : par qui ? quelle application ? quelle machine ?
La base de Prometheus ne répond pas à du SQL, mais à du PromQL : un langage adaptée à cette notion de label.
Celui-ci se présente sous le format suivant: NOM_METRIQUES{LABEL1="VALEUR",LABEL2="valeur"}[durée]
- En SQL:
SELECT node_memory_MemFree_bytes WHERE instance IS "nodeexporter:9100" AND WHERE TIME >= Dateadd(MINUTE, -5, GETDATE())
- En PromQL:
node_memory_MemFree_bytes{instance="nodeexporter:9100"}[5m]
Les avantages et les inconvénients
Avantages:
- Peut s’intégrer à de nombreuses solutions grâce aux exporters tiers (+ les exporters sont faciles à créer).
- Gère les alertes* lui-même.
- Possible d’avoir x Prometheus agregés par un Prometheus central (ex: un Prometheus par zone/DC).
Inconvénients:
- Pas de notion de cluster (l’agrégation ne crée pas de la HA).
- Pas adapté pour stocker des données textuelles (uniquement les métriques).
- Peu de sécurité (uniquement TLS et authentification en ‘basic_auth’).
Installation de Prometheus
Prometheus est disponible dans la plupart des dépôts :
apt install prometheus
apk add prometheus
yum install prometheus
Il est aussi possible de récupérer une version récente depuis l’onglet Release du dépôt Github officiel.
En démarrant le service Prometheus, un serveur web sera accessible au port 9090.
Information
Je vais utiliser le domaine prometheus.home qui est géré par mon DHCP/DNS local. L’usage d’un nom de domaine est facultatif mais sera essentiel dans une prochaine section (support du TLS).
Mon serveur Prometheus utilise l’adresse suivante : http://server.prometheus.home:9090
Configurer Prometheus
Si vous installez la version sous forme de paquet, vous aurez un fichier de configuration disponible à cet emplacement : /etc/prometheus/config.yml
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
À ce stade, la partie qui nous interesse est scrape_config
puisque c’est à cet emplacement que nous allons ajouter les exporters.
On remarque d’ailleurs que Prometheus se monitore lui-même. Nous pouvons déjà commencer à écrire nos premières requêtes en PromQL.
Information
Pour info, voici les caractéristiques de mon installation:
- OS: Alpine 3.17.1
- Prometheus: 2.47.1
- NodeExporter: 1.6.1 (à voir plus tard)
- AlertManager: 0.26.0 (à voir plus tard)
- PushGateway 1.6.2 (à voir plus tard)
Si ce n’est pas déjà fait, il faut démarrer le service Prometheus.
service prometheus start # AlpineOS
systemctl start prometheus
Pour voir les métriques de Prometheus, il suffit de faire une requête sur le chemin suivant : /metrics
.
curl http://server.prometheus.home:9090/metrics -s | tail -n 5
# HELP promhttp_metric_handler_requests_total Total number of scrapes by HTTP status code.
# TYPE promhttp_metric_handler_requests_total counter
promhttp_metric_handler_requests_total{code="200"} 20
promhttp_metric_handler_requests_total{code="500"} 0
promhttp_metric_handler_requests_total{code="503"} 0
Depuis l’interface web il est possible d’afficher les métriques disponibles et de les requêter.
Information
Il existe un utilitaire nommé promtool permettant de tester la configuration de Prometheus et de faire des requêtes en PromQL.
Exemple de requête via PromTool:
promtool query instant http://localhost:9090 'prometheus_http_requests_total{code="200", handler="/graph", instance="localhost:9090", job="prometheus"}'
prometheus_http_requests_total{code="200", handler="/graph", instance="localhost:9090", job="prometheus"} => 2 @[1696592187.208]
Le PromQL
Qu’est-ce que le PromQL ?
Avant de continuer à ajouter des exporters, je vais faire un petit tour du PromQL.
PromQL, ou Prometheus Query Language, est un langage de requête spécialement conçu pour interroger, analyser et extraire des données à partir d’une base de données de séries temporelles gérée par Prometheus. Ce dernier est un système de surveillance open-source largement utilisé qui collecte et stocke des métriques de performances ainsi que des données de séries temporelles à partir de diverses sources telles que des serveurs, des applications, des services, et plus encore.
PromQL permet aux utilisateurs de formuler des requêtes complexes pour extraire des informations utiles à partir des métriques collectées par Prometheus. Voici quelques-uns des principaux concepts et fonctionnalités de PromQL :
Séries temporelles : Les métriques dans Prometheus sont stockées sous forme de séries temporelles, qui sont des flux de points de données indexés par un ensemble de labels (étiquettes). Par exemple, une série temporelle pourrait représenter l’utilisation du CPU d’un serveur à un instant donné.
Sélection de métriques : Vous pouvez utiliser PromQL pour sélectionner des métriques spécifiques en fonction de critères tels que les labels, les noms de métriques, etc. Par exemple, vous pouvez interroger toutes les métriques liées au CPU d’une instance particulière en précisant son architecture et/ou son nom.
Agrégation : PromQL prend en charge des opérations d’agrégation telles que la somme, la moyenne, le maximum, le minimum, etc. Vous pouvez agréger les données sur une période de temps donnée pour obtenir des statistiques.
Opérations mathématiques : Vous pouvez effectuer des opérations mathématiques sur les métriques, ce qui vous permet de créer de nouvelles séries temporelles en combinant ou en transformant les données existantes.
Créer une requête
Une requête PromQL peut ressembler à ceci :
node_network_transmit_bytes_total
Cette règle sélectionne toutes les métriques ayant le nom node_network_transmit_bytes_total
, peu importe sa provenance, son contexte, ou ses labels. On obtient alors une liste de réponses provenant de toutes les machines monitorées par Prometheus.
Il nous est alors possible d’ajouter un filtre pour préciser le contexte de la métrique :
node_network_transmit_bytes_total{instance="laptop.prometheus.home", device="wlp46s0"}
Nous ciblons maintenant l’instance laptop.prometheus.home
et la carte réseau wlp46s0
. Nous obtenons une seule réponse puisque ces précédents filtres ciblent une machine et une carte.
Cibler toutes les métriques possédant un label
Il est possible de faire une requête sur toutes les métriques possédant un label précis. Par exemple, je souhaite récupérer toutes les métriques dont le label instance
est laptop.prometheus.home
.
{instance="laptop.prometheus.home"}
Regex et négation
Une fonctionnalité très importante du PromQL est l’intégration des regex. Il est possible de faire des requêtes sur des métriques dont le label correspond à une expression régulière en rajoutant un tild (~
) avant la valeur.
apt_upgrades_pending{arch=~"^a[a-z]+"}
Sans passer par une regex, on peut exclure un label avec un point d’exclamation (!
).
apt_upgrades_pending{arch!="i386"}
On peut même combiner les deux pour exclure une regex !
apt_upgrades_pending{arch!~"i[0-9]{3}"}
Revenir dans le temps
Nous sommes maintenant capable de récupérer des métriques mais également d’en récuperer des plus anciennes avec le mot clé offset
.
node_network_transmit_bytes_total{instance="laptop.prometheus.home", device="wlp46s0"} offset 5m
On obtient les métriques de la carte réseau wlp46s0
de la machine laptop.prometheus.home
datant de 5 minutes.
Je peux aussi récupérer toutes les valeurs depuis 5 minutes.
node_network_transmit_bytes_total{instance="laptop.prometheus.home", device="wlp46s0"}[5m]
La réponse est la suivante :
87769521 @1696594464.484
87836251 @1696594479.484
87957452 @1696594494.484
88027802 @1696594509.484
88394773 @1696594524.484
88861454 @1696594539.484
90392775 @1696594554.484
91519657 @1696594569.484
(J’ai un peu raccourci le résultat.)
Ces valeurs sont séparées par un @
et sont au format valeur @ timestamp
.
Mais cette métrique n’est pas très pertinente puisqu’il est évident que la valeur augmente au fil du temps.
Il est alors possible de faire des opérations mathématiques sur les métriques et notamment d’en sortir un graphique avec la fonction rate
qui a pour but de calculer le taux de croissance d’une métrique.
rate(node_network_transmit_bytes_total{instance="laptop.prometheus.home", device="wlp46s0"}[5m])
Dans ce cas, on convertit une valeur de type counter
en gauge
, nous allons voir comment cela fonctionne dans la prochaine section.
Les types de métriques
Il existe 4 types de métriques dans Prometheus :
- Counter : une valeur qui augmente au fil du temps. Exemple : le nombre de requêtes HTTP 200 sur un serveur web, celui-ci ne peut pas diminuer.
- Gauge : une valeur qui peut augmenter ou diminuer au fil du temps. Exemple : la quantité de mémoire utilisée par un processus.
- Histogram : une valeur qui peut augmenter ou diminuer au fil du temps, mais qui est aussi capable de stocker des valeurs dans des buckets (des intervalles). Exemple : le nombre de requêtes HTTP 200 sur un serveur web, mais avec des intervalles de temps (0-100ms, 100-200ms, etc).
- Summary : identique aux Histogram mais ne nécessitant pas de connaître les intervalles à l’avance.
Il est possible de passer d’un type à un autre en appliquant une fonction comme rate
(vu juste au dessus, permettant de créer une courbe à partir d’un taux de croissance)
Notre premier exporter (NodeExporter)
Qu’est-ce que NodeExporter ?
Nous avons un Prometheus qui récupère les métriques d’un exporter (lui-même) à l’adresse : 127.0.0.1:9090.
Nous allons maintenant pouvoir ajouter un exporter plus pertinent qui va récupérer les métriques de notre système.
Habituellement, le premier exporter à installer sur une machine est le NodeExporter.
Je ne le ferai pas pour chaque exporter présenté mais voici les métriques disponibles sur le NodeExporter :
Métriques
- Processeur :
- node_cpu_seconds_total : Temps total passé par le processeur en mode utilisateur, système et inactif.
- node_cpu_usage_seconds_total : Temps total passé par le processeur en mode utilisateur, système et inactif, exprimé en pourcentage.
- node_cpu_frequency_average : Fréquence moyenne du processeur.
- node_cpu_temperature : Température du processeur.
- Mémoire :
- node_memory_MemTotal_bytes : Quantité totale de mémoire physique disponible.
- node_memory_MemFree_bytes : Quantité de mémoire physique libre.
- node_memory_Buffers_bytes : Quantité de mémoire utilisée pour les tampons.
- node_memory_Cached_bytes : Quantité de mémoire utilisée pour le cache.
- node_memory_SwapTotal_bytes : Quantité totale de mémoire virtuelle disponible.
- node_memory_SwapFree_bytes : Quantité de mémoire virtuelle libre.
- Disque :
- node_disk_io_time_seconds_total : Temps total passé par les disques en lecture et écriture.
- node_disk_read_bytes_total : Quantité totale de données lues par les disques.
- node_disk_write_bytes_total : Quantité totale de données écrites par les disques.
- node_disk_reads_total : Nombre total de lectures de disque.
- node_disk_writes_total : Nombre total d’écritures de disque.
- Réseau :
- node_network_receive_bytes_total : Quantité totale de données reçues par le réseau.
- node_network_transmit_bytes_total : Quantité totale de données transmises par le réseau.
- node_network_receive_packets_total : Nombre total de paquets reçus par le réseau.
- node_network_transmit_packets_total : Nombre total de paquets transmis par le réseau.
- Système d’exploitation :
- node_kernel_version : Version du noyau Linux.
- node_os_name : Nom de l’OS.
- node_os_family : Famille de l’OS.
- node_os_arch : Architecture de l’OS.
- Matériel :
- node_machine_type : Type de machine.
- node_cpu_model : Modèle du processeur.
- node_cpu_cores : Nombre de cœurs du processeur.
- node_cpu_threads : Nombre de threads du processeur.
- node_disk_model : Modèle du disque.
- node_disk_size_bytes : Taille du disque.
- node_memory_device : Dispositif mémoire.
Installation du NodeExporter
Installation sur Linux
Pour installer le NodeExporter, il y a plusieurs méthodes. La plus simple est d’utiliser le paquet officiel :
apt install prometheus-node-exporter
apk add prometheus-node-exporter
yum install prometheus-node-exporter
Astuce
Installation sur Docker
La plupart des exporters sont aussi disponibles sous forme de conteneur Docker. C’est le cas du NodeExporter :
docker run -d \
--net="host" \
--pid="host" \
-v "/:/host:ro,rslave" \
quay.io/prometheus/node-exporter:latest \
--path.rootfs=/host
Installation manuelle
Il est également possible de récupérer une version récente depuis l’onglet Release du dépôt Github officiel.
Installation sur Windows
Windows ne possède pas le même programme que ses confrères linuxiens. Celui-ci est developpé par la communauté mais ne possède pas les mêmes noms de métriques : Windows Exporter.
Configuration du NodeExporter
Si le service n’est pas démarré automatiquement, il faut le faire :
service prometheus-node-exporter start # AlpineOS
systemctl start prometheus-node-exporter
Ce service va ouvrir un port sur le 9100. Nous pourrons voir nos métriques à l’adresse suivante : http://server.prometheus.home:9100/metrics
(en admettant que cet exporter soit sur le même serveur que Prometheus).
On peut voir que les métriques sont bien présentes, mais il faut maintenant les ajouter à notre fichier de configuration /etc/prometheus/config.yml
pour que Prometheus en tienne compte.
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets:
# - alertmanager:9093
rule_files:
# - "first_rules.yml"
# - "second_rules.yml"
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "node"
static_configs:
- targets: ["localhost:9100"]
Après un reload du service Prometheus, nous pouvons voir que les métriques du NodeExporter sont bien présentes sur la page Status->Targets (ou http://server.prometheus.home:9090/targets
).
Tentons maintenant une requête pour voir si tout fonctionne bien :
round((node_filesystem_size_bytes{device="/dev/vg0/lv_root", instance="localhost:9100"} - node_filesystem_avail_bytes{device="/dev/vg0/lv_root", instance="localhost:9100"}) / node_filesystem_size_bytes{device="/dev/vg0/lv_root", instance="localhost:9100"} * 100)
Cette requête permet de calculer le pourcentage utilisé sur le volume /dev/vg0/lv_root
. (taille totale - taille disponible) / taille totale * 100
, le tout arrondi à l’entier le plus proche.
On remarque bien que nous précisons l’instance localhost:9100
et le device /dev/vg0/lv_root
pour récupérer les métriques du bon volume. Cela veut dire que nous pouvons voir les informations d’une autre machine en modifiant cette valeur par la valeur “instance” correspondant à un autre exporter configuré dans Prometheus.
Je vais d’ores et déjà ajouter un autre exporter pour récupérer les métriques de mon PC portable.
- job_name: "node"
static_configs:
- targets: ["localhost:9100"]
- targets: ["laptop.prometheus.home:9100"]
labels:
owner: "Quentin"
room: "office"
➜ df -h | grep "data-root"
/dev/mapper/data-root 460G 408G 29G 94% /
Et comme vous l’avez vu, il est possible d’ajouter des labels pour préciser le contexte de la métrique. Ces labels seront utiles dans notre PromQL.
Je vais compléter notre terrain de jeu en ajoutant d’autres exporters.
Par exemple: ((node_network_transmit_bytes_total / node_network_receive_bytes_total) > 0) and {room="office", device!~"(tinc.*)|lo|(tap|tun).*|veth.*|fw.*|br.*"}
permet de récupérer le ratio de données envoyées par rapport aux données reçues de toutes les machines présente dans le bureau en excluant les interfaces de type tinc
, lo
, tap
, tun
, veth
, fw
et br
.
Comme pour le SQL, il est possible de faire des fonctions de groupes (sum, avg, min, max, count, etc) et des fonctions mathématiques (round, floor, ceil, etc).
Le Relabeling
Le relabeling est une fonctionnalité de Prometheus permettant de modifier les labels d’une métrique sans modifier l’exporter lui-même.
Son fonctionnement est simple:
- On prend un label existant (source_label);
- On y applique un traitement (suppression, remplacement, ajout, etc);
- On stocke le résultat dans un nouveau label (target_label).
Il existe aussi des méta-labels qui rendent accessible certains paramètres de l’exporter.
Par exemple : __address__
contient l’adresse de l’exporter (qui créera le label instance
), __scheme__
contient le protocole utilisé (http ou https), etc.
Un relabeling peut être utilisé pour cacher le port utilisé par l’exporter.
- job_name: "node"
static_configs:
# ...
- targets: ["bot.prometheus.home:9100"]
labels:
room: "office"
virtual: "yes"
- targets: ["proxmox.prometheus.home:9100"]
labels:
room: "office"
virtual: "no"
relabel_configs:
- source_labels: [__address__]
regex: '(.*):[0-9]+'
replacement: '${1}'
target_label: instance
Cette modification est basique mais permet déjà de simplifier nos requêtes lorsqu’on souhaite cibler une machine précise. Après l’ajout d’autres exporters sur la même machine, l’instance sera la même pour tous les exporters présents sur celle-ci.
Il est aussi possible de modifier le nom d’une métrique :
- job_name: "node"
static_configs:
# ...
- targets: ["bot.prometheus.home:9100"]
labels:
room: "office"
virtual: "yes"
- targets: ["proxmox.prometheus.home:9100"]
labels:
room: "office"
virtual: "no"
metric_relabel_configs:
- source_labels: ['__name__']
regex: 'node_boot_time_seconds'
target_label: '__name__'
replacement: 'node_boot_time'
Avec cette configuration, je cherche toutes les métriques dont le label __name__
est node_boot_time_seconds
et je le remplace par node_boot_time
.
Monitorer un port/site/IP (Blackbox Exporter)
Jusque là, nous n’avons monitoré que des métriques d’hôtes sur lesquels nous avions un accès pour y installer un exporter. Mais comment monitorer une entité distante sur laquelle nous n’avons pas la main ?
La solution est la suivante : un exporter n’a pas forcément besoin d’être sur l’hôte à monitorer. L’exporter MySQL peut être sur un serveur dédié à la supervision et monitorer une base de données distante.
Il existe alors un exporter permettant de monitorer un port, un site web ou une IP : Blackbox Exporter.
apt install prometheus-blackbox-exporter
apk add prometheus-blackbox-exporter
yum install prometheus-blackbox-exporter
Le fichier de configuration est le suivant : /etc/prometheus/blackbox.yml
.
modules:
http_2xx:
prober: http
http:
preferred_ip_protocol: "ip4"
http_post_2xx:
prober: http
http:
method: POST
tcp_connect:
prober: tcp
pop3s_banner:
prober: tcp
tcp:
query_response:
- expect: "^+OK"
tls: true
tls_config:
insecure_skip_verify: false
grpc:
prober: grpc
grpc:
tls: true
preferred_ip_protocol: "ip4"
grpc_plain:
prober: grpc
grpc:
tls: false
service: "service1"
ssh_banner:
prober: tcp
tcp:
query_response:
- expect: "^SSH-2.0-"
- send: "SSH-2.0-blackbox-ssh-check"
irc_banner:
prober: tcp
tcp:
query_response:
- send: "NICK prober"
- send: "USER prober prober prober :prober"
- expect: "PING :([^ ]+)"
send: "PONG ${1}"
- expect: "^:[^ ]+ 001"
icmp:
prober: icmp
icmp_ttl5:
prober: icmp
timeout: 5s
icmp:
ttl: 5
Il n’est pas necéssaire de modifier la configuration par défaut. Ce seront des paramètres à préciser dans la requête.
curl "http://server.prometheus.home:9115/probe?target=https://une-tasse-de.cafe&module=http_2xx" -s | tail -n 12
# HELP probe_ssl_last_chain_expiry_timestamp_seconds Returns last SSL chain expiry in timestamp
# TYPE probe_ssl_last_chain_expiry_timestamp_seconds gauge
probe_ssl_last_chain_expiry_timestamp_seconds 1.703592167e+09
# HELP probe_ssl_last_chain_info Contains SSL leaf certificate information
# TYPE probe_ssl_last_chain_info gauge
probe_ssl_last_chain_info{fingerprint_sha256="1ad4423a139029b08944fe9b42206cc07bb1b482b959d3908c93b4e4ccec7ed8",issuer="CN=R3,O=Let's Encrypt,C=US",subject="CN=une-tasse-de.cafe",subjectalternative="une-tasse-de.cafe"} 1
# HELP probe_success Displays whether or not the probe was a success
# TYPE probe_success gauge
probe_success 1
# HELP probe_tls_version_info Returns the TLS version used or NaN when unknown
# TYPE probe_tls_version_info gauge
probe_tls_version_info{version="TLS 1.3"} 1
Les paramètres utilisés sont les suivants:
- module : le module à utiliser (défini dans le fichier de configuration)
- target : l’adresse à monitorer
Autre exemple pour ping d’une adresse IP : il faut utiliser le module icmp
et préciser l’adresse IP dans le paramètre target
.
curl 'http://server.prometheus.home:9115/probe?target=1.1.1.1&module=icmp' -s | tail -n 6
# HELP probe_ip_protocol Specifies whether probe ip protocol is IP4 or IP6
# TYPE probe_ip_protocol gauge
probe_ip_protocol 4
# HELP probe_success Displays whether or not the probe was a success
# TYPE probe_success gauge
probe_success 1
Maintenant, nous pouvons ajouter cet exporter à notre fichier de configuration Prometheus en précisant les cibles à monitorer.
Voici un exemple de configuration qui va monitorer les entités suivantes :
- Les sites https://une-tasse-de.cafe et https://une-pause-cafe.fr.
- Les IPs 1.1.1.1, 192.168.1.250 et 192.168.1.400.
- Le port 32400 de l’hôte media_server.
- job_name: 'websites'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://une-tasse-de.cafe
- https://une-pause-cafe.fr
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
regex: 'http.://(.*)' # supprimer le http(s) dans l'instance, sinon mettre à (.*) pour garder l'adresse complète
replacement: '${1}'
target_label: instance
- target_label: __address__ # Interroger le blackbox exporter
replacement: 127.0.0.1:9115
- job_name: 'icmp'
metrics_path: /probe
params:
module: [icmp]
static_configs:
- targets:
- 1.1.1.1
- 192.168.1.250
- 192.168.1.400 # existe pas
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
regex: '(.*)'
replacement: '${1}'
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9115
- job_name: 'tcp_port'
metrics_path: /probe
params:
module: [tcp_connect]
static_configs:
- targets:
- media_server:32400
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- target_label: __address__
replacement: 127.0.0.1:9115
- source_labels: [__param_target]
regex: '(.*)'
target_label: instance
Avertissement
Dès lors que nous ajoutons un exporter, nous avons une métrique up qui lui est assignée.
Cette métrique indique si un exporter est accessible ou non, cela ne veut pas dire que l’hôte monitoré est fonctionnel !
Exemple: l’IP 192.168.1.400 n’existe pas, mais l’exporter BlackBox est accessible. La requête up est OK, mais dès que je demande le résultat du ping celui-ci est à 0 (donc l’hôte ne répond pas).
promtool query instant http://localhost:9090 "up{instance='192.168.1.400'}"
up{instance="192.168.1.250", job="icmp"} => 1 @[1696577858.881]
promtool query instant http://localhost:9090 "probe_success{instance='192.168.1.400'}"
probe_success{instance="192.168.1.400", job="icmp"} => 0 @[1696577983.835]
Où trouver des exporters ?
La documentation officielle de Prometheus propose une liste d’exporters : prometheus.io/docs/instrumenting/exporters/.
La liste est assez complète, mais vous pouvez également trouver des SDKs et des exemples pour développer vos propres exporters directement sur le Github de Prometheus : github.com/prometheus.
Exemple d’exporter en Golang
Voici un exemple d’exporter en golang permettant de compter le nombre d’adresses IP dans le fichier /etc/hosts
. Il ouvre le port 9108 et expose la métrique etc_hosts_ip_count
à l’emplacement /metrics
.
Code
package main
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
ipCount := prometheus.NewGauge(prometheus.GaugeOpts{
Name: "etc_hosts_ip_count",
Help: "Nombre d'adresses IP dans le fichier /etc/hosts",
})
prometheus.MustRegister(ipCount)
fileContent, err := ioutil.ReadFile("/etc/hosts")
if err != nil {
fmt.Println(err)
}
ipCountValue := countIPs(string(fileContent))
ipCount.Set(float64(ipCountValue))
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Nombre d'adresses IP dans /etc/hosts: %d", ipCountValue)
})
fmt.Println("Démarrage du serveur HTTP sur le port :9108")
fmt.Println(http.ListenAndServe(":9108", nil))
}
func countIPs(content string) int {
lines := strings.Split(content, "\n")
ipCount := 0
for _, line := range lines {
if strings.TrimSpace(line) == "" || strings.HasPrefix(line, "#") {
continue
}
fields := strings.Fields(line)
for _, field := range fields {
if isIPv4(field) || isIPv6(field) {
ipCount++
}
}
}
return ipCount
}
func isIPv4(s string) bool {
parts := strings.Split(s, ".")
if len(parts) != 4 {
return false
}
for _, part := range parts {
if len(part) == 0 {
return false
}
for _, c := range part {
if c < '0' || c > '9' {
return false
}
}
num := atoi(part)
if num < 0 || num > 255 {
return false
}
}
return true
}
func isIPv6(s string) bool {
return strings.Count(s, ":") >= 2 && strings.Count(s, ":") <= 7
}
func atoi(s string) int {
n := 0
for _, c := range s {
n = n*10 + int(c-'0')
}
return n
}
Une fois l’exporter compilé et lancé, nous l’ajoutons à Prometheus :
scrape_configs:
- job_name: "etc_hosts"
static_configs:
- targets: ["laptop.prometheus.home:9108"]
promtool query instant http://localhost:9090 'etc_hosts_ip_count'
etc_hosts_ip_count{instance="laptop.prometheus.home:9108", job="etc_hosts"} => 4 @[1696597793.314]
Il est évidemment possible de faire la même chose avec un script Python, Java, Node, etc.
Les recording rules
Une recording rule est une simplification d’une requête PromQL. Elle fonctionne de la même manière qu’une requête mais est exécutée en amont et stockée dans une nouvelle métrique.
C’est un moyen d’optimiser les requêtes que l’on exécute régulièrement et ainsi de ne pas surcharger Prometheus (notammment lorsque l’on est dans le cadre d’un dashboard qui va répéter le même code PromQL à chaque intervalle de raffraichissement).
Les recordings rules doivent être créées dans un ou plusieurs fichiers. Par exemple, je vais créer un fichier nodes.yml
dans le dossier /etc/prometheus/rules/
(dossier à créer avant) et y ajouter les règles suivantes :
groups:
- name: node_exporter_recording_rules
rules:
- record: node_memory_usage
expr: (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) / node_memory_MemTotal_bytes * 100
- record: node_cpu_usage
expr: 100 - (avg by (instance) (rate(node_cpu_seconds_total{job="node"}[5m])) * 100)
- record: node_network_connections
expr: avg_over_time(node_network_connections{job="node"}[5m])
- record: node_disk_usage
expr: (node_filesystem_size_bytes - node_filesystem_avail_bytes) / node_filesystem_size_bytes * 100
Les noms des règles doivent être unique et respecter la regex [a-zA-Z_:][a-zA-Z0-9_:]*
(même règle que pour les métriques classiques).
Après ça, il faut ajouter le fichier de règles dans la configuration de Prometheus :
global:
scrape_interval: 15s
evaluation_interval: 15s
# ...
rule_files: # <--- référence les fichiers de règles
- "single_file.yml"
- "rules/*.yml"
Après un reload de Prometheus, nous pouvons voir que les règles sont bien présentes dans l’onglet Status->Rules.
Ces règles sont utilisables dans nos requêtes PromQL, nous pouvons rajouter des filtres comme pour les métriques classiques.
Les alertes
J’en ai parlé au début de cet article : Prometheus est capable de nous alerter lorsqu’une métrique dépasse un certain seuil. C’est une fonctionnalité très importante pour un outil de supervision.
Mais … je vous ai menti. Prometheus va bien déclarer des alertes mais est incapable de nous les envoyer via un quelconque moyen. Il faut donc utiliser un autre outil pour gérer l’envoi des alertes. Le rôle de Prometheus est juste de “déclarer” ses alertes à l’outil choisi une fois les seuils critiques atteints.
Créer une alerte
Les alertes sont déclarées dans les mêmes fichiers que les Les recording rules. Je crée donc le répertoire /etc/prometheus/alerts/
et y ajoute le fichier nodes.yml
avec le contenu suivant :
groups:
- name: node_rules
rules:
- alert: any:exporter:down
expr: >
( up == 0 )
labels:
severity: low
- alert: any:node:down
expr: >
( up{job="node"} == 0 )
labels:
severity: moderate
- alert: datacenter:burning
expr: count(up{job="node"}) == 0
labels:
severity: world-ending
for: 5m
Ces alertes sont visibles dans l’onglet Status->Alerts.
any:exporter:down
: alerte si un exporter est down.any:node:down
: alerte si un exporter de node est down.datacenter:burning
: alerte si tous les exporters de node sont down.
Si je stoppe le service NodeExporter sur mon PC portable, je vois bien que les alertes any:exporter:down
et any:node:down
sont déclenchées.
Maintenant… comment faire pour recevoir ces alertes ?
AlertManager
AlertManager est un outil permettant de gérer les alertes de Prometheus. Il est capable de recevoir les alertes de ce dernier et de les envoyer via différents supports (email, slack, webhook, etc). Il est également capable de gérer les silences et de grouper les alertes.
Installation
AlertManager est disponible dans les dépôts officiels de la plupart des distributions Linux.
apt install prometheus-alertmanager
apk add alertmanager
yum install prometheus-alertmanager
AlertManager est aussi instalable via Docker :
docker run -d -p 9093:9093 \
--name alertmanager \
-v /etc/alertmanager/config.yml:/etc/alertmanager/config.yml \
prom/alertmanager
Ou via l’archive disponible sur le Github officiel : github.com/prometheus/alertmanager/releases.
Une fois démarré, il est accessible sur le port 9093. Il est possible de le configurer via le fichier /etc/alertmanager/config.yml
.
AlertManager doit être déclaré dans la configuration de Prometheus :
global:
scrape_interval: 5s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets:
- server.prometheus.home:9093 # <---- ici !
rule_files:
- "rules/*.yml"
- "alerts/*.yml"
# ...
Et à partir du moment où Prometheus sera redémarré, il enverra les alertes à AlertManager dès lors qu’elles dépassent les seuils définis dans les fichiers de règles.
Dans mon cas, j’ai configuré des alertes qui envoient des emails via mon serveur SMTP :
receivers:
- name: 'web.hook'
webhook_configs:
- url: 'http://127.0.0.1:5001/'
- name: 'email'
email_configs:
- to: 'contact@mailserver'
from: 'no-reply@mailserver'
smarthost: 'mailserver:587'
auth_username: 'no-reply@mailserver'
auth_identity: 'no-reply@mailserver'
auth_password: 'bigpassword'
Inhibit Rules
Maintenant, l’alerte any:exporter:down
n’est pas pertinente si une autre alerte d’une plus grande importance est déclenchée (comme par exemple any:node:down
). C’est là qu’interviennent les inhibit rules dans AlertManager.
inhibit_rules:
- source_matchers:
- severity = moderate
target_matchers:
- severity = low
equal: ['instance']
Avec cette configuration, si une alerte de severity moderate
est déclenchée alors toutes les alertes de severity low
seront inhibées pour peu qu’elles aient le même label instance
(ce qui necéssite un relabeling si ce n’est pas le même exporter).
Maintenant, si je stoppe le service NodeExporter sur mon PC portable, je vois bien que l’alerte any:exporter:down
est inhibée par l’alerte any:node:down
.
Sur Prometheus, nos deux alertes sont bien déclenchées :
Sur AlertManager, seule l’alerte any:node:down
est visible :
Routage des alertes
AlertManager est capable de router les alertes en fonction de leur label. Par exemple, je souhaite que les alertes de severity low
soient envoyées par un support A, et les alertes de severity moderate
soient envoyées sur un support B.
Je considère que les mails sont dédiés aux alertes importantes. Je vais donc créer un nouveau receiver pour les alertes low
et modifier le receiver email
pour les alertes moderate
.
J’ai tendance à utiliser Gotify dans mon usage habituel, mais n’étant pas chez moi durant l’édition de cet article, je vais utiliser Discord comme alternative.
J’ajoute donc discord en tant que receiver dans le fichier /etc/alertmanager/alertmanager.yml
:
receivers:
- name: 'discord'
discord_configs:
- webhook_url: 'https://discord.com/api/webhooks/1160256074398568488/HT18QHDiqNOwoQL7P2XFAhnOoASYFyX-bSKtLM1EZMA2812Nb2kUMRzr7BiHmhO1amHY'
Les labels étant déjà présents dans les alertes Prometheus, il suffit de créer une route pour chaque severity dans le fichier /etc/alertmanager/alertmanager.yml
:
route:
group_by: ['alertname']
group_wait: 20s
group_interval: 5m
repeat_interval: 3h
receiver: email # par défaut
routes:
- matchers:
- severity =~ "(low|info)"
receiver: discord
Maintenant, les alertes de severity low
et info
seront envoyées sur Discord et les autres seront envoyées par mail.
Personnaliser les alertes
Changer l’objet des emails
Il est possible de personnaliser les alertes en utilisant des templates. Le format est le même que Jinja2 (utilisé par Ansible) ou GoTmpl.
Dans mon cas, la template par défaut pour les emails ne convient pas vraiment. Je souhaite avoir un objet plus explicite et un corps de mail en français.
- Template par défaut :
Pour cela, le fichier alertmanager.yml
doit être mis à jour avec le contenu suivant :
- name: 'email'
email_configs:
- to: 'contact@mailserver'
from: 'no-reply@mailserver'
smarthost: 'mailserver:587'
auth_username: 'no-reply@mailserver'
auth_identity: 'no-reply@mailserver'
auth_password: 'bigpassword'
headers:
Subject: "[ {{ .Alerts | len }} alerte{{ if gt (len .Alerts) 1 }}s{{ end }}] - {{ range .GroupLabels.SortedPairs }}{{ .Name }}={{ .Value }}{{ end }}"
J’utilise l’header Subject
pour modifier l’objet du mail. J’utilise également la fonction len
pour compter le nombre d’alertes et ainsi mettre “alerte” au pluriel si besoin.
Changer le corps des emails
Je ne souhaite pas reprendre entièrement la template par défaut pour le corps des emails. Je souhaite juste traduire le texte en français.
Je vais donc télécharger la template par défaut, disponible sur le Github officiel d’AlertManager.
wget https://raw.githubusercontent.com/prometheus/alertmanager/main/template/email.tmpl -O /etc/alertmanager/email.tmpl
Je modifie le nom de la template à l’intérieur du fichier de “email.default.subject” à “email.subject” pour que le nom soit différent de celui de la template par défaut. Je modifie également le contenu de la template pour avoir un corps de mail en français.
Fichier template
{{ define "email.subject" }}{{ template "__subject" . }}{{ end }}
{{ define "email.html" }}
<html xmlns="http://www.w3.org/1999/xhtml" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>{{ template "__subject" . }}</title>
<style>
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1,
h2,
h3,
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body itemscope itemtype="https://schema.org/EmailMessage" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 1.6em; background-color: #f6f6f6; width: 100%;">
<table class="body-wrap" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #f6f6f6; width: 100%;" width="100%" bgcolor="#f6f6f6">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;" valign="top"></td>
<td class="container" width="600" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block; max-width: 600px; margin: 0 auto; clear: both;" valign="top">
<div class="content" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; margin: 0 auto; display: block; padding: 20px;">
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #fff; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="#fff">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
{{ if gt (len .Alerts.Firing) 0 }}
<td class="alert alert-warning" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #E6522C;" valign="top" align="center" bgcolor="#E6522C">
{{ .Alerts | len }} alerte{{ if gt (len .Alerts) 1 }}s{{ end }} de {{ range .GroupLabels.SortedPairs }}
{{ .Name }}={{ .Value }}
{{ end }}
</td>
{{ else }}
<td class="alert alert-good" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #68B90F;" valign="top" align="center" bgcolor="#68B90F">
{{ .Alerts | len }} alerte{{ if gt (len .Alerts) 1 }}s{{ end }} de {{ range .GroupLabels.SortedPairs }}
{{ .Name }}={{ .Value }}
{{ end }}
</td>
{{ end }}
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-wrap" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 30px;" valign="top">
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<a href="{{ template "__alertmanagerURL" . }}" class="btn-primary" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; text-decoration: none; color: #FFF; background-color: #348eda; border: solid #348eda; border-width: 10px 20px; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize;">Ouvrir {{ template "__alertmanager" . }}</a>
</td>
</tr>
{{ if gt (len .Alerts.Firing) 0 }}
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">[{{ .Alerts.Firing | len }}] Firing</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Firing }}
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Labels</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
{{ if gt (len .Annotations) 0 }}<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Annotations</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
<a href="{{ .GeneratorURL }}" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline;">Source</a><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
</td>
</tr>
{{ end }}
{{ if gt (len .Alerts.Resolved) 0 }}
{{ if gt (len .Alerts.Firing) 0 }}
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<hr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
</td>
</tr>
{{ end }}
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">[{{ .Alerts.Resolved | len }}] Resolved</strong>
</td>
</tr>
{{ end }}
{{ range .Alerts.Resolved }}
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Labels</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
{{ if gt (len .Annotations) 0 }}<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Annotations</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
<a href="{{ .GeneratorURL }}" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline;">Source</a><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
</td>
</tr>
{{ end }}
</table>
</td>
</tr>
</table>
<div class="footer" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; padding: 20px;">
<table width="100%" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<td class="aligncenter content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; padding: 0 0 20px; text-align: center; color: #999; font-size: 12px;" valign="top" align="center"><a href="{{ .ExternalURL }}" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; text-decoration: underline; color: #999; font-size: 12px;">Sent by {{ template "__alertmanager" . }}</a></td>
</tr>
</table>
</div></div>
</td>
<td style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;" valign="top"></td>
</tr>
</table>
</body>
</html>
{{ end }}
- Template modifiée :
Modifier les URLs dans l’email reçu
Lorsque vous avez reçu un email d’alerte, vous avez peut-être remarqué que les URLs ne sont pas accessibles puisqu’il vous proposera l’URL http://prometheus:9090
pour Prometheus et http://alertmanager:9093
pour AlertManager.
Notamment sur le bouton Source et Ouvrir AlertManager
Pour cela, il faut démarrer Prometheus et AlertManager avec les options --web.external-url
et --web.route-prefix
.
Voici les valeurs par défaut :
--web.external-url
:http://localhost:9090
--web.route-prefix
:/
En fonction de la méthode via laquelle vous voulez accéder à Prometheus et AlertManager, il faudra adapter ces valeurs (notamment si vous êtes derrière un reverse-proxy).
Dans mon cas, je souhaite accéder à Prometheus et AlertManager via l’URL http://server.prometheus.home:9090
et http://server.prometheus.home:9093
.
Il est obligatoire de modifier le service OpenRC ou Systemd pour ajouter les options --web.external-url
(et --web.route-prefix
si vous le souhaitez).
Pour OpenRC, je modifie le fichier /etc/conf.d/prometheus
pour mettre une valeur dans la variable prometheus_arg
:
prometheus_config_file=/etc/prometheus/prometheus.yml
prometheus_storage_path=/var/lib/prometheus/data
prometheus_retention_time=15d
prometheus_args="--web.external-url='http://server.prometheus.home:9090'"
output_log=/var/log/prometheus.log
error_log=/var/log/prometheus.log
Dans le cadre d’un service systemd, je modifie le fichier /etc/default/prometheus
pour mettre une valeur dans la variable ARGS
:
ARGS="--web.external-url='http://server.prometheus.home:9090'"
Pour AlertManager, c’est la même chose. Je modifie le fichier /etc/conf.d/alertmanager
pour mettre une valeur dans la variable alertmanager_arg
:
alertmanager_args="--web.external-url='http://server.prometheus.home:9093'"
alertmanager_config_file=/etc/alertmanager/alertmanager.yml
alertmanager_storage_path=/var/lib/alertmanager/data
output_log=/var/log/alertmanager.log
error_log=/var/log/alertmanager.log
Dans le mail final : les URLS vers Prometheus et AlertManager ne redirigent plus vers http://prometheus:9090
et http://alertmanager:9093
mais bien vers http://server.prometheus.home:9090
et http://server.prometheus.home:9093
.
PushGateway
Jusque là nous avons vu comment récupérer des métriques sur des hôtes distants. Mais quid des cas où nous avons besoin de récupérer des métriques sur un hôte qui n’est pas accessible par Prometheus (par exemple une ip dynamique) ou qui n’est pas censé être accessible 24/7 ?
Comme pour un script qui va être exécuté une fois par jour et qui va récupérer des métriques, ou encore un script qui va être exécuté à chaque démarrage d’un conteneur Docker.
À cette problématique il existe une solution : PushGateway
C’est un service qui à pour objectif de récupérer des métriques via des requêtes POST, et de les ajouter à son exporter que Prometheus pourra requêter.
Les tâches utilisant PushGateway ne sont pas contraintes d’être lancée durant un Pull de Prometheus. PushGateway joue le rôle d’un intermédiaire.
Installation
PushGateway est disponible dans les dépôts officiels de la plupart des distributions Linux :
apt install prometheus-pushgateway
apk add prometheus-pushgateway
yum install prometheus-pushgateway
Ou via Docker :
docker run -d -p 9091:9091 prom/pushgateway
Ou via l’archive disponible sur le Github officiel : github.com/prometheus/pushgateway/releases.
Utilisation
Comme expliqué ci-dessus, PushGateway a pour rôle de récupérer des métriques provenant de scripts ou d’applications qui n’ont pas pour objectif d’être accessible 24/7.
Pour cela, nous allons envoyer des requêtes HTTP à PushGateway pour qu’il les stocke. Il est possible de le faire via un script Bash, Python, Node, etc.
/metrics/job/<JOB_NAME>{/<LABEL_NAME>/<LABEL_VALUE>}
Via le code suivant, je souhaite envoyer la métrique disk_temperature_celsius
contenant la température de mon disque NVME.
#!/bin/bash
hostname=$(cat /etc/hostname)
kelvin=$(sudo nvme smart-log /dev/nvme0 -o json | jq '.["temperature_sensor_1"]')
celsius=$(echo "$kelvin - 273.15" | bc)
echo "La température du disque /dev/nvme0 est ${celsius}°C"
cat <<EOF | curl --data-binary @- http://server.prometheus.home:9091/metrics/job/disk_temperature_celsius/instance/capteur
# TYPE disk_temperature_celsius gauge
disk_temperature_celsius{instance="${hostname}"} ${celsius}
EOF
Depuis l’interface web nous pouvons voir que la métrique est bien présente :
Chaque métrique pushée est stockée dans PushGateway jusqu’à ce qu’elle soit écrasée par une nouvelle métrique avec le même nom. Il est donc possible de pusher plusieurs métriques avec le même nom mais avec des labels différents.
PushGateway va exposer ces métriques sur le port 9091 avec le chemin /metrics
.
curl http://server.prometheus.home:9091/metrics -s | grep "disk_temperature_celsius"
# TYPE disk_temperature_celsius gauge
disk_temperature_celsius{instance="capteur",job="disk_temperature_celsius"} 49.85
push_failure_time_seconds{instance="capteur",job="disk_temperature_celsius"} 0
push_time_seconds{instance="capteur",job="disk_temperature_celsius"} 1.6966899829492722e+09
Maintenant il faut ajouter PushGateway à la configuration de Prometheus :
- job_name: "PushGateway"
static_configs:
- targets: ["server.prometheus.home:9091"]
relabel_configs:
- source_labels: [__address__]
regex: '(.*):[0-9]+'
replacement: '${1}'
target_label: instance
Nous pouvons voir que la métrique est bien récupérée par Prometheus :
Prometheus et la sécurité
Ici, nous allons voir comment sécuriser les échanges entre Prometheus et les exporters.
Pour sécuriser un Prometheus, il existe plusieurs solutions :
- L’authentification basic_auth.
- Chiffrer les échanges avec TLS.
- Utiliser un pare-feu pour limiter les accès des exporters (ou reverse proxy).
Nous allons voir comment mettre en place les 2 premières solutions.
Activer l’authentification basic_auth
Sur Prometheus
Dans un premier temps, il est nécessaire de générer le mot de passe chiffré avec la commande htpasswd
ou via le script Python gen-pass.py disponible dans la documentation officielle de Prometheus.
import getpass
import bcrypt
password = getpass.getpass("password: ")
hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
print(hashed_password.decode())
Je génère mon mot de passe (qui sera ici Coffee
) :
python3 gen-pass.py
password:
$2b$12$W1jWulWeeF2LECdJMZc2G.i5fvJGSl5CE8F6Flz3tW4Uz7yBdUWZ.
Maintenant nous allons créer un fichier yaml qui contiendra la configuration du serveur web de Prometheus. Celui-ci est séparé de la configuration principale et pourra être réutilisé pour d’autres services (exporters, alertmanager, etc).
Création du répertoire:
mkdir -p /etc/prometheus/web
Fichier web.yml
:
basic_auth_users:
admin: $2b$12$W1jWulWeeF2LECdJMZc2G.i5fvJGSl5CE8F6Flz3tW4Uz7yBdUWZ.
ops: $2b$12$W1jWulWeeF2LECdJMZc2G.i5fvJGSl5CE8F6Flz3tW4Uz7yBdUWZ.
Il faut également modifier les arguments de démarrage de Prometheus pour activer l’authentification basic_auth en ajoutant --web.config.file=/etc/prometheus/web/web.yml
comme durant la configuration de l’URL externe.
Je modifie le fichier /etc/conf.d/prometheus
pour ajouter notre argument. Voici donc mon fichier final :
prometheus_config_file=/etc/prometheus/prometheus.yml
prometheus_storage_path=/var/lib/prometheus/data
prometheus_retention_time=15d
prometheus_args="--web.external-url='http://server.prometheus.home:9090' --web.config.file=/etc/prometheus/web/web.yml"
output_log=/var/log/prometheus.log
error_log=/var/log/prometheus.log
Si vous utilisez un service systemd, il faudra modifier le fichier /etc/default/prometheus
pour ajouter notre argument. Voici un exemple :
ARGS="--web.external-url='http://server.prometheus.home:9090' --web.config.file=/etc/prometheus/web/web.yml"
Après un redémarrage de Prometheus, il est possible de se connecter avec les identifiants admin
ou ops
:
Maintenant… je viens de recevoir un mail concernant une certaine alerte any:exporter:down
me disant que mon exporter node-exporter
est down.
Pourquoi ? Parce que Prometheus n’arrive plus à s’auto-monitorer. Il n’arrive plus à accéder à localhost:9090/metrics
et c’est normal puisque nous venons d’activer l’authentification basic_auth
.
Nous devons modifier notre inventaire d’exporters pour y ajouter l’authentification :
- job_name: "prometheus"
basic_auth:
username: 'admin'
password: 'Coffee'
static_configs:
- targets: ["localhost:9090"]
Sur les exporters
La plupart des exporters officiels respectent la même syntaxe et peuvent même réutiliser le même fichier de configuration.
Sur chaque machine possédant un exporter, je vais créer le répertoire /etc/exporters/web
et y créer le fichier web.yml
avec le contenu suivant :
basic_auth_users:
prom: $2b$12$W1jWulWeeF2LECdJMZc2G.i5fvJGSl5CE8F6Flz3tW4Uz7yBdUWZ.
J’utiliserai alors les identifiants prom
et Coffee
pour me connecter.
Comme pour Prometheus, il faut ajouter l’argument --web.config.file=/etc/exporters/web/web.yml
dans les arguments de démarrage de l’exporter. Sur les Alpines, il faut modifier le fichier /etc/conf.d/node-exporter
comme ci-dessous :
# /etc/conf.d/node-exporter
# Custom arguments can be specified like:
#
# ARGS="--web.listen-address=':9100'"
ARGS="--web.config.file=/etc/exporters/web/web.yml"
Si vous utilisez un service systemd, il faudra modifier le fichier /etc/default/prometheus-node-exporter
:
# Set the command-line arguments to pass to the server.
# Due to shell scaping, to pass backslashes for regexes, you need to double
# them (\\d for \d). If running under systemd, you need to double them again
# (\\\\d to mean \d), and escape newlines too.
ARGS="--web.config.file=/etc/exporters/web/web.yml"
Avertissement
Attention, j’ai constaté qu’en fonction de la version de votre exporter il est possible que l’argument –web.config.file ne soit pas disponible. Dans ce cas, il faudra utiliser l’argument –web.config.
Pour Node-Exporter 1.6.1:
ARGS="--web.config.file=/etc/exporters/web/web.yml"
Pour Node-Exporter 1.3.1:
ARGS="--web.config=/etc/exporters/web/web.yml"
Après un redémarrage de l’exporter, il faut bien se connecter avec les identifiants prom
; Coffee
.
- job_name: "node"
basic_auth:
username: 'prom'
password: 'Coffee'
static_configs:
- targets: ["localhost:9100"]
- targets: ["dhcp.prometheus.home:9100"]
- targets: ["bot.prometheus.home:9100"]
- targets: ["proxmox.prometheus.home:9100"]
relabel_configs:
- source_labels: [__address__]
regex: '(.*):[0-9]+'
replacement: '${1}'
target_label: instance
Activer le chiffrement TLS
Dans mon homelab, j’utilise MKCert pour générer des certificats TLS ainsi que des autorités de certification qui seront ajoutés sur les machines de mon réseau.
Installation de MKCert
MKCert est disponible dans les dépôts officiels de la plupart des distributions Linux.
apt install mkcert
apk add mkcert
yum install mkcert
Générer une autorité de certification
Pour que les certificats soient considérés comme valides par Prometheus, il faut que l’autorité de certification soit ajoutée au système de confiance (trust-store) de notre hôte.
mkcert -install
Created a new local CA 💥
The local CA is now installed in the system trust store! ⚡️
Il est possible de copier notre CA pour l’ajouter sur d’autres machines :
mkcert -CAROOT
/root/.local/share/mkcert
Générer un certificat
Deux solutions s’offrent à nous :
- Générer un certificat pour chaque exporter.
- Générer un certificat wildcard pour tous les exporteurs.
Pour ma part, est choisie la première solution (plus sécurisé, et réaliste dans un environnement de production).
for subdomain in dhcp proxmox server bot; do mkcert "${subdomain}.prometheus.home" ; done
Chaque certificat sera généré dans le répertoire courant.
prometheus:~/certs# ls -l
total 24
-rw------- 1 root root 1708 Oct 8 07:52 dhcp.prometheus.home-key.pem
-rw-r--r-- 1 root root 1472 Oct 8 07:52 dhcp.prometheus.home.pem
-rw------- 1 root root 1704 Oct 8 07:52 proxmox.prometheus.home-key.pem
-rw-r--r-- 1 root root 1476 Oct 8 07:52 proxmox.prometheus.home.pem
-rw------- 1 root root 1704 Oct 8 07:52 server.prometheus.home-key.pem
-rw-r--r-- 1 root root 1472 Oct 8 07:52 server.prometheus.home.pem
Installer un certificat
Ajouter un certificat sur un exporter
Pour installer un certificat, je vais créer le répertoire /etc/exporters/certs
et y copier les certificats.
mkdir -p /etc/exporters/certs
mv *-key.pem /etc/exporters/certs/key.pem
mv *.home.pem /etc/exporters/certs/cert.pem
chown -R prometheus:prometheus /etc/exporters/certs
Maintenant, je peux éditer le fichier /etc/exporters/web/web.yml
(déjà créé durant l’étape ajoutant la basic_auth ) pour donner les chemins vers les certificats.
basic_auth_users:
prom: $2b$12$W1jWulWeeF2LECdJMZc2G.i5fvJGSl5CE8F6Flz3tW4Uz7yBdUWZ.
tls_server_config:
cert_file: /etc/exporters/certs/cert.pem
key_file: /etc/exporters/certs/key.pem
L’argument --web.config.file=/etc/exporters/web/web.yml
est déjà présent dans la commande de démarrage de l’exporter si vous avez suivi la partie Activer l’authentification basic_auth.
Après un redémarrage de l’exporter je peux tenter un accès via HTTPS :
curl https://server.prometheus.home:9100 -u "prom:Coffee" -o /dev/null -v
Je constate alors que la connexion est bien chiffrée :
* SSL connection using TLSv1.3 / TLS_CHACHA20_POLY1305_SHA256
* ALPN: server accepted h2
* Server certificate:
* subject: O=mkcert development certificate; OU=root@prometheus
* start date: Oct 8 05:52:53 2023 GMT
* expire date: Jan 8 06:52:53 2026 GMT
* subjectAltName: host "server.prometheus.home" matched cert's "server.prometheus.home"
* issuer: O=mkcert development CA; OU=root@prometheus; CN=mkcert root@prometheus
* SSL certificate verify ok.
Dernière étape pour que Prometheus puisse se connecter à l’exporter via HTTPS : ajouter scheme: https
dans l’inventaire des exporters.
- job_name: "node"
scheme: "https" # <--- Ajout de cette ligne
basic_auth:
username: 'prom'
password: 'Coffee'
static_configs:
- targets: ["server.prometheus.home:9100"]
- targets: ["dhcp.prometheus.home:9100"]
- targets: ["bot.prometheus.home:9100"]
- targets: ["proxmox.prometheus.home:9100"]
relabel_configs:
- source_labels: [__address__]
regex: '(.*):[0-9]+'
replacement: '${1}'
target_label: instance
Après un redémarrage de Prometheus, je constate que les métriques sont bien récupérées via HTTPS !
Ajouter un certificat sur Prometheus
Dans le fichier /etc/prometheus/web/web.yml
(créé à l’étape sur les basic_auth) je vais ajouter les chemins vers les certificats, comme pour les exporters.
basic_auth_users:
admin: $2b$12$W1jWulWeeF2LECdJMZc2G.i5fvJGSl5CE8F6Flz3tW4Uz7yBdUWZ.
ops: $2b$12$W1jWulWeeF2LECdJMZc2G.i5fvJGSl5CE8F6Flz3tW4Uz7yBdUWZ.
tls_server_config:
cert_file: /etc/exporters/certs/cert.pem
key_file: /etc/exporters/certs/key.pem
Après un redémarrage de Prometheus, je peux tenter un accès via HTTPS depuis mon poste. Le certificat n’est pas considéré comme valide puisque je n’ai pas ajouté l’autorité de certification sur ma machine.
➜ curl https://server.prometheus.home:9090
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
Je récupère alors le rootCA.pem et l’ajoute dans mon système de confiance (trust-store).
scp [email protected]:/root/.local/share/mkcert/rootCA.pem .
sudo cp ./rootCA.pem /usr/local/share/ca-certificates/mkcert.crt
sudo update-ca-certificates
Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt,it does not contain exactly one certificate or CRL
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
Adding debian:mkcert.pem
done.
done.
Maintenant, je peux me connecter à Prometheus via HTTPS :
curl https://server.prometheus.home:9090 -u "admin:Coffee"
<a href="/graph">Found</a>.
Il est aussi possible de ne pas ajouter l’autorité de certification sur le système de confiance et d’uniquement l’importer dans notre Firefox pour pouvoir se connecter à Prometheus via HTTPS. (Settings -> Certificates -> View Certificates -> Authorities -> Import)
Agrégation de métriques
Il d’usage de créer un serveur Prometheus par “Zone” ou par “Cluster” (Kube) afin que chaque environnement soit indépendant. Mais avoir une vue d’ensemble de tous ces environnements peut vite devenir compliqué.
C’est pourquoi Prometheus propose une fonctionnalité d’agrégation de métriques qui permet de récupérer les données de plusieurs serveurs Prometheus et de les agréger sur un serveur principal.
Dans mon cas, j’ai :
- Un Prometheus dans un cluster Kubernetes hébergé sur mes Raspberry Pi.
- Un deuxième Prometheus dans un cluster Kubernetes dans des machines virtuelles (cluster de test).
- Un troisième Prometheus qui va monitorer les hôtes (serveurs physiques, VMs, etc).
Je souhaite récupérer les métriques des Prometheus dans les clusters Kubernetes et les agréger sur le Prometheus qui monitore les hôtes.
Avertissement
Je vous conseille de cibler les métriques que vous souhaitez récupérer. En effet, si vous récupérez toutes les métriques de tous les Prometheus, vous risquez de saturer la mémoire vive de votre serveur principal.
- job_name: 'cloudkube'
scrape_interval: 15s
honor_labels: true
metrics_path: '/federate'
params:
'match[]':
- '{job="kubernetes"}' # pour tout prendre: {job=~".*"}
static_configs:
- targets: ['192.168.128.51:30310']
relabel_configs:
- replacement: 'cloudkube'
target_label: instance
Astuce
honor_labels: true
Cette instruction permet de gérer les conflits lorsqu’un serveur fedéré possède des métriques avec les mêmes labels qu’un autre serveur.
- Si la valeur est true, on force l’ajout des labels du serveur fedéré (quite à écraser les labels existants).
- Si la valeur est false, les métriques du serveur fedéré seront renommées en exported_NOM. (“exported_job”, “exported_instance” etc.)
Conclusion
Je pense que nous avons fait le tour de Prometheus et de ses fonctionnalités principales. Il reste néanmoins beaucoup de choses que je n’ai pas abordées dans cet article (usage d’un service discovery, API d’administration, Stockage long-terme avec Thanos etc.), mais je pense que cette page est suffisament longue.
Personnellement, je trouve que Prometheus est un produit très intéressant et propose une capacité d’adaptation hallucinante. L’usage des exporters fait sa force, et est son principal défaut : si une machine cumule les services, elle nécessitera un grand nombre d’exporters (et sera complexe à administrer). J’ai également été déçu de trouver de nombreux exporters communautaires qui ne respectent pas la même syntaxe (et fichiers de configuration) que les exporters officiels, rendant l’usage du TLS et basic_auth complexes (voire impossibles sur certains).
La documentation de Prometheus est très accessible, bien qu’il manque quand même de nombreux exemples sur certains usages.
N’hésitez pas à essayer Prometheus vous-même afin de vous faire votre propre opinion.
Si vos retours sont bons concernant cet article et que vous souhaitez que j’approfondisse certains points, n’hésitez pas à me le faire savoir afin que nous puissions aborder ces nouvelles thématiques ensemble.