Tu vois, c’est là qu’elle a triché la fille…

Christophe, regardant ses magnifiques graphes Munin

Munin, c’est loin d’être parfait, mais ça a un avantage non négligeable : c’est ultra léger. Si tu as besoin de sortir des métriques rapidement pour un système auto-hébergé, ça évite de sortir la très grosse artillerie tout produisant des données utilisables et utiles, pour une complexité relativement réduite.

Il y a quelques jours, je me suis demandé comment je pourrais produire des graphes pour Garage (parce que garage, c’est bien, mangez-en) histoire d’avoir des infos que je considère comme essentielles : quantité de données par bucket, nombre d’objets et éventuellement téléversements foireux.

Toutes ces données sont accessibles via la commande garage bucket info <bucket>, mais je cherchais a pouvoir grapher l’ensemble de manière un peu plus efficace.

Métriques dans le code de Garage

Première piste suivie : regarder ce qui est existe déjà dans garage. Étant un jeune logiciel, il intègre par défaut tout un tas de protocoles et de standards modernes pour interagir avec d’autres éléments assez communs dans les Systèmes d’Information modernes : consul, kubernetes, etc… Ce qui nous intéresse ici est la partie métrique qui est disponible au format OpenTelemetry.

Pour l’activer, rien de plus simple, dans /etc/garage.toml, il suffit d’ajouter deux lignes pour activer l’API admin :

[admin]
api_bind_addr = "[::1]:3903"

On redémarre garage et on peut alors accéder directement aux métriques via l’URL http://[::1]:3903/metrics.

Alors, c’est cool, on peut y trouver pas mal de métriques internes à garage, mais malheureusement, absolument rien qui concerne les buckets eux-mêmes. Dans un contexte de supervision, la documentation officielle donne même les indicateurs qu’il faut suivre en priorité, et les éventuels soucis qu’ils peuvent représenter.

Sauf que c’est pas du tout ce qu’on veut, on veut simplement grapher l’usage.

Le code source de Garage ?

garage est écrit en Rust, j’ai donc jeté un petit coup d’œil dans le code. Autant le dire tout de suite : c’est bien au-dessus de mes compétences et si j’ai réussi, à force d’essais et d’erreurs, à introduire des données dans les métriques déjà supportées, en ajouter de nouvelles est une autre paire de manches !

Sans aller jusqu’à dire que c’est impossible (encore une fois, je ne connais que très peu le code et il est d’une complexité relativement élevée pour moi), je dirais que ce n’est pas forcément la bonne approche.

Après avoir fait le tour de ça, je suis donc revenu à la base : quand on tape garage bucket info <bucket>, on obtient bien les données que l’on cherche, elles doivent donc bien être disponibles quelque part…

Évidemment la première option, ce serait de parser le résultat de cette commande, mais elle n’est visiblement pas vraiment faite pour être parsée (en tout cas, pas facilement), petit exemple :

# garage bucket info tarace
Bucket: cdfe[…]28f0

Size: 43.6 GiB (46.8 GB)
Objects: 374678
Unfinished multipart uploads: 1497

Website access: true

Global aliases:
  tarace
  ta.mere.lol

Key-specific aliases:

Authorized keys:
  RW   GKXXXX  tarace-key

Il y a des espaces partout et il est a priori impossible d’obtenir les unités brutes pour le stockage (elles sont systématiquement calculées en MiB/GiB/TiB avant d’être affichées). On peut évidemment, et j’ai vérifié, bricoler le code source pour lui faire afficher ce que l’on souhaite, mais une modif juste pour ça me paraît très largement au-dessus de la nécessité.

Et c’est là que je me suis souvenu que garage avait aussi une API d’administration sur laquelle on pouvait trouver pas mal de trucs…

API admin, quand tu nous tiens

Activer l’API admin est relativement simple, il suffit d’ajouter un token dans /etc/garage.toml :

[admin]
api_bind_addr = "[::1]:3903"
admin_token = "tamerelol"

À partir de ce moment, on peut interroger l’API avec un petit curl des familles, tout simplement (l’entête doit juste contenir Bearer <admin_token> dans un champs Authorization et le tour est joué) :

> curl -s -H "Authorization: Bearer tamerelol" "http://[::1]:3903/v0/status"
{
  "node": "153d5ffd1f2be62c1eec4f318ba862e4aefcb28be6bc778422d22ada69cef904",
  "garageVersion": "git:v0.8.1-209-g1ecd88c-modified",
  "garageFeatures": [
    "k2v",
    "sled",
    "metrics",
    "bundled-libs"
  ],
[…]
}

Et donc, il suffit d’utiliser le endpoint bucket pour récupérer les infos dont on a besoin :

> curl -s -H "Authorization: Bearer tamerelol" "http://[::1]:3903/v0/bucket"
[
  {
    "id": "dd8113bace4a0f65721f88f5b386b50dff3ac62f3e7ac4bc002570d4c4c98fe2",
    "globalAliases": [
      "tamerelol"
    ],
    "localAliases": []
  }
]
> curl -s -H "Authorization: Bearer tamerelol" "http://[::1]:3903/v0/bucket?id=dd8113bace4a0f65721f88f5b386b50dff3ac62f3e7ac4bc002570d4c4c98fe2"
{
  "id": "dd8113bace4a0f65721f88f5b386b50dff3ac62f3e7ac4bc002570d4c4c98fe2",
  "globalAliases": [
    "tamerelol"
  ],
  "websiteAccess": false,
  "websiteConfig": null,
  "keys": [
    {
      "accessKeyId": "GK94ecde83f07248fb33b81d34",
      "name": "Unnamed key",
      "permissions": {
        "read": true,
        "write": true,
        "owner": true
      },
      "bucketLocalAliases": []
    }
  ],
  "objects": 24,
  "bytes": 19420604,
  "unfinishedUploads": 12,
  "quotas": {
    "maxSize": null,
    "maxObjects": null
  }
}

À partir de ce moment-là, c’est essentiellement une question de rédiger un script correct pour Munin.

Chargez les Munin-tions (oui, c’est très mauvais)

Le prérequis pour faire un script Munin est assez minimaliste en réalité. Il faut :

  • avoir un morceau de script capable de renvoyer les caractéristiques des graphes si on lui ajout l’entrée config ;
  • être capable de sortir un chiffre par ligne dans un format assez basique.

C’est donc simplement une affaire de récupérer les bonnes données, les mettre en forme et merci kiki.

On va ici simplement s’attaquer à la donnée qui concerne les objets dans les buckets. Les autres graphes auront exactement le même script et la même forme, il s’agit simplement de changer quelques données dedans à chaque fois (j’expliquerai un peu plus loin quelques subtilités).

Histoire d’avoir un truc présentable, on va aussi essayer de faire afficher à Munin les noms des buckets plutôt que leur identifiant. À noter qu’avec l’API admin de garage, il est impossible d’appeler le endpoint bucket avec un nom. Seuls les identifiants sont acceptés. On va donc utiliser du bash avec un tableau associatif (histoire de pouvoir la translation identifiant/nom plus facilement).

Première chose à faire donc, stocker en « sécurité » notre admin_token pour qu’il soit accessible depuis tous nos scripts Munin. Pour cela, on peut aller l’ajouter comme une variable d’environnement pour les scripts commençant par garage* dans /etc/munin/plugin-conf.d/garage :

[garage*]
env.BEARER tamerelol

La variable shell ${BEARER} contiendra systématiquement ce token quand on l’exécute dans l’environnement de Munin. Voilà le résultat après une grosse heure de script :

#!/bin/bash

HEADER="Authorization: Bearer ${BEARER}" # on forme l’entête un peu en avance,
                                         # on va s’en resservir souvent

declare -A BUCKETS=() # on déclare le tableau associatif

# on va aller récupérer l’ensemble des buckets via l’API.
# On utilise jq pour interpréter les résultats et les stocker sous forme :
# <id>,<name>
# un par ligne
API_BUCKETS_JSON=$(curl -s -H "${HEADER}" "http://[::1]:3903/v0/bucket" | jq -r '.[] | .id + "," + .globalAliases[0]')

# on va peupler notre tabeau associatif en interprétant bêtement chaque ligne
for bucket in ${API_BUCKETS_JSON}
do
	BUCKETS+=([$(echo ${bucket} | cut -d ',' -f 1)]="$(echo ${bucket} | cut -d ',' -f 2)")
done

# Ceci sert uniquement à la partie configuration :
# on va y poser les labels, le type de graphe, ainsi que le nom de chaque
# courbe. Ce nom sera donc automatiquement le nom du bucket S3.
case $1 in
	config)
		cat << 'EOM'
graph_title Objects by Bucket
graph_vlabel Number of objects
graph_args --base 1000 -l 0
graph_category garage
EOM
		for i in "${!BUCKETS[@]}"
		do
			echo "${BUCKETS[${i}]}.label ${BUCKETS[${i}]}"
		done
		exit 0;;
esac

# ceci est un exécution normale du script
# on y refait un appel à l’API mais avec l’identifiant du bucket cette fois-ci
# comme ça, on peut récupérer la donnée qui nous intéresse, toujours via jq
# et l’afficher proprement !
for i in "${!BUCKETS[@]}"
do
	OBJECTS=$(curl -s -H "${HEADER}" "http://[::1]:3903/v0/bucket?id=${i}" | jq -r '.objects')
	echo "${BUCKETS[${i}]}.value ${OBJECTS}"
done

Dans le cas d’une exécution avec configuration, on obtient donc ceci :

> ./garage_bucket_objects config
graph_title Objects by Bucket
graph_vlabel Number of objects
graph_args --base 1000 -l 0
graph_category garage
tamerelol.label tamerelol

Et avec une exécution standard :

> ./garage_bucket_objects
tamerelol.value 24

Si l’on ajoute un nouveau bucket, youpi, il est automatiquement pris en compte :

> ./garage_bucket_objects
youpi.value 0
tamerelol.value 24

On peut alors tester le script en condition réelle sur le serveur garage, en le plaçant dans /etc/munin/plugins, via :

munin-run garage_bucket_objects

Si tout se passe bien, il suffit alors de redémarrer le service munin-node et le tour est joué !

Et pour… le reste ?

Si tu souhaite grapher d’autres données, c’est à peu près aussi simple : lors de second appel, sans rien modifier d’autres, tu peux simplement demander le champs unfinishedUploads au lieu du champs objects pour obtenir les téléversements non terminés.

Si tu veux obtenir la taille de chaque bucket, c’est globalement la même chose à un petit détail près : non seulement il faut effectivement demander le champs bytes lors du second appel à l’API, mais il faut aussi demander au graphe de passer d’une base 1000 à une base 1024 histoire d’afficher des données cohérentes.

Mais voilà, c’est à peu près tout, ça fait des jolis graphes et tout le monde est content \o/

Bonus : grapher le total

Petit bonus de dernière minute : pour grapher un total (somme de toutes les courbes), il suffit d’ajouter ce qui suit à la config.

graph_total Total

Pour du disque, ça peut être pertinent…