Logo
Overview
Un Talos européen de qualité - Part VIII - Logs & Traces

Un Talos européen de qualité - Part VIII - Logs & Traces

October 31, 2025
14 min read
part-08

Objectif 🎯

Nous avons vu la partie métrique précédemment, et il est temps de s’attaquer au logging et à la traçabilité. Nous allons commencer par installer les backends de stockage Loki pour les logs et Tempo pour les traces, tout 2 en mode distribué, puis nous verrons comment collecter efficacement ces données avec Alloy.

Voici le résultat attendu en terme d’architecture de la stack de télémétrie, en excluant la partie métrique déjà vue :

Schéma Télémétrie

Backends de stockage 🗄️

Logging 📇

clusters/dev-kube/module-monitoring.tf
module "kube_monitoring" {
source = "../../modules/kube/monitoring"
loki_s3_endpoint = "https://${local.s3_endpoint}"
loki_s3_region = local.s3_region
loki_s3_bucket = local.cluster_name
loki_s3_access_key = var.loki_s3_username
loki_s3_secret_key = var.loki_s3_password
}
Explanation

Pour le stockage des logs long terme, Loki a besoin d’un stockage S3.

Appliquer la config avec terraform apply. Vérifier que tout est bien déployé avec kgp -n logging.

Loki fourni son propre dashboard Grafana :

Dashboard Loki

Mais il sera vide en l’état actuel, car nous n’avons aucun outil de collecte de données pour le moment. On enchaîne tout de suite sur l’installation du backend Tempo.

Traces 🔍

clusters/dev-kube/module-monitoring.tf
module "kube_monitoring" {
// ...
tempo_s3_endpoint = local.s3_endpoint
tempo_s3_region = local.s3_region
tempo_s3_bucket = local.cluster_name
tempo_s3_access_key = var.tempo_s3_username
tempo_s3_secret_key = var.tempo_s3_password
}
Explanation

De même que pour Loki, nous allons utiliser le mode distribué adapté pour la production. De ce fait un stockage S3 est requis.

Appliquer la config avec terraform apply. Vérifier que tout est bien déployé avec kgp -n tracing.

Collecte 📜

Bon cool, on a nos backends, mais aucun collector pour alimenter tout ça. C’est ici que Alloy entre en jeu.

Afin de distribuer au mieux la collecte des données, assez massives quand il s’agit de logs et traces, Alloy est devenu un composant de choix dans l’écosystème de l’observabilité. Il fournit une solution complète centralisée et flexible pour la collecte, le traitement et le routage des logs, métriques et traces.

L’utilisation de Prometheus Operator via les CRDs ServiceMonitor/PodMonitor (mode pull/scraping) exclu l’utilisation d’Alloy pour la collecte des métriques de l’infra, hors métriques OLTP (mode push) encore rarement utilisé. Il nous servira donc principalement pour les logs et traces. Pour la partie logs, il remplace pleinement Promtail, l’outil historique pour la collecte des logs.

modules/kube/monitoring/alloy.tf
resource "kubernetes_namespace_v1" "telemetry" {
metadata {
name = "telemetry"
labels = {
"pod-security.kubernetes.io/enforce" = "privileged"
}
}
}
resource "kubernetes_config_map_v1" "alloy" {
metadata {
name = "alloy"
namespace = kubernetes_namespace_v1.telemetry.metadata[0].name
}
data = {
"config.alloy" = <<EOF
discovery.kubernetes "pod" {
role = "pod"
}
discovery.relabel "pod" {
targets = discovery.kubernetes.pod.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
action = "replace"
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_name"]
action = "replace"
target_label = "pod"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_name"]
action = "replace"
target_label = "container"
}
rule {
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
action = "replace"
target_label = "app"
}
rule {
source_labels = ["__meta_kubernetes_namespace", "__meta_kubernetes_pod_container_name"]
action = "replace"
target_label = "job"
separator = "/"
replacement = "$1"
}
rule {
source_labels = ["__meta_kubernetes_pod_uid", "__meta_kubernetes_pod_container_name"]
action = "replace"
target_label = "__path__"
separator = "/"
replacement = "/var/log/pods/*$1/*.log"
}
rule {
source_labels = ["__meta_kubernetes_pod_container_id"]
action = "replace"
target_label = "container_runtime"
regex = "^(\\S+):\\/\\/.+$"
replacement = "$1"
}
}
local.file_match "pod" {
path_targets = discovery.relabel.pod.output
}
loki.source.file "pod" {
targets = local.file_match.pod.targets
forward_to = [loki.process.pod.receiver]
}
loki.process "pod" {
stage.cri {}
stage.static_labels {
values = {
cluster = "local",
}
}
forward_to = [loki.write.endpoint.receiver]
}
loki.write "endpoint" {
endpoint {
url = "http://loki-gateway.logging/loki/api/v1/push"
}
}
otelcol.receiver.otlp "default" {
http {}
grpc {}
output {
metrics = [otelcol.processor.batch.default.input]
traces = [otelcol.processor.batch.default.input]
logs = [otelcol.processor.batch.default.input]
}
}
otelcol.processor.batch "default" {
output {
metrics = [otelcol.exporter.otlphttp.prometheus.input]
logs = [otelcol.exporter.otlphttp.loki.input]
traces = [otelcol.exporter.otlp.tempo.input]
}
}
otelcol.exporter.otlphttp "prometheus" {
client {
endpoint = "http://prometheus-operated.monitoring:9090/api/v1/otlp"
}
}
otelcol.exporter.otlphttp "loki" {
client {
endpoint = "http://loki-gateway.logging/otlp"
}
}
otelcol.exporter.otlp "tempo" {
client {
endpoint = "tempo-distributor.tracing:4317"
tls {
insecure = true
}
}
}
EOF
}
}
resource "helm_release" "alloy" {
repository = "https://grafana.github.io/helm-charts"
chart = "alloy"
version = "1.5.2"
name = "alloy"
namespace = kubernetes_namespace_v1.telemetry.metadata[0].name
max_history = 2
set = [
{
name = "serviceMonitor.enabled"
value = "true"
},
{
name = "alloy.configMap.create"
value = "false"
},
{
name = "alloy.configMap.name"
value = kubernetes_config_map_v1.alloy.metadata[0].name
},
{
name = "alloy.configMap.key"
value = "config.alloy"
},
{
name = "alloy.extraPorts[0].name"
value = "otlp-http"
},
{
name = "alloy.extraPorts[0].port"
value = "4318"
},
{
name = "alloy.extraPorts[0].targetPort"
value = "4318"
},
{
name = "alloy.extraPorts[1].name"
value = "otlp-grpc"
},
{
name = "alloy.extraPorts[1].port"
value = "4317"
},
{
name = "alloy.extraPorts[1].targetPort"
value = "4317"
},
{
name = "alloy.mounts.varlog"
value = "true"
},
{
name = "controller.tolerations[0].operator"
value = "Exists"
},
]
}
resource "kubernetes_manifest" "traefik_ingress_route_alloy" {
manifest = {
apiVersion = "traefik.io/v1alpha1"
kind = "IngressRoute"
metadata = {
name = "alloy"
namespace = kubernetes_namespace_v1.telemetry.metadata[0].name
}
spec = {
entryPoints = ["internal"]
routes = [
{
match = "Host(`alloy.${var.internal_domain}`)"
kind = "Rule"
middlewares = [
{
name = "internal-basic-auth"
namespace = "traefik"
}
]
services = [
{
name = "alloy"
port = "http-metrics"
}
]
}
]
}
}
}
Explanation

Par rapport à Promtail, il y a beaucoup plus de config à faire, prix de la flexibilité ?

Côté Helm on indique le ConfigMap à utiliser, et on expose les ports OTLP HTTP et gRPC pour activer la collecte des traces. Les applications supportant OpenTelemetry devront envoyer leurs données de spans/traces sur ces ports (ce qui est déjà le cas sur Traefik installé précédemment).

Quant aux logs, on utilise l’API Kubernetes pour aller chercher les pods et leurs containers sur lesquels Alloy devra collecter les logs, puis on applique une série de règles de relabellisation pour avoir des labels cohérents et exploitables dans Grafana.

L’exemple fourni par la doc officielle récupère le stream des logs via l’API Kubernetes. Cela a l’avantage de ne pas nécessiter de déploiement d’agent sur chaque nœud, ni d’élévation de privilège. Cependant, cela n’est pas du tout optimal en termes de charge sur l’API server ainsi que niveau réseau.

Je préfère personnellement rester sur l’approche Promtail traditionnelle de récupérer les logs directement à la source au niveau fichier brut. Dans ce mode, au niveau du chart helm, 2 éléments essentiels sont à considérer :

  • On s’assure via controller.tolerations que les pods alloy se déploient sur l’ensemble des noeuds. Cela est nécessaire pour la collecte des logs au niveau fichier, de la même manière que pour Promtail.
  • On monte le volume /var/log local de chaque noeud dans les pods alloy via alloy.mounts.varlog. Ceci nécessite des privilèges élevés indiqués dans le namespace.

Il s’agit ensuite de remplacer loki.source.kubernetes par le combo local.file_match et loki.source.file, qui permette respectivement de matcher les fichiers de logs grâce à __path__ et de parser les fichiers de logs.

local.file_match "pod" {
path_targets = discovery.relabel.pod.output
}
loki.source.file "pod" {
targets = local.file_match.pod.targets
forward_to = [loki.process.pod.receiver]
}

En mode fichier, dans loki.process, il est important d’indiquer stage.cri afin de parser les logs au format CRI (le format standard utilisé par les runtimes de containers comme containerd ou Docker).

En résumé, côté config :

  • On récupère tous les pods actifs de l’API Kubernetes.
  • On applique toute une stratégie de labellisation pour avoir un truc propre à exploiter.
  • On matche les fichiers de logs via __path__ pour chaque container de chaque pod puis on parse les logs au format CRI.
  • On précise le point d’entrée loki-gateway.logging de Loki pour l’écriture des logs.
  • On configure un récepteur OTLP par défaut pour recevoir les données OpenTelemetry. Sur ce récepteur, nous utilisons trois processeurs batch distincts pour les métriques, les traces et les logs :
    • Pour la partie metrics, on forwarde vers l’endpoint OTLP de prometheus préalablement configuré au chapitre précédent http://prometheus-operated.monitoring:9090/api/v1/otlp.
    • Pour la partie logs, on forwarde vers l’URL loki http://loki-gateway.logging/otlp.
    • Pour la partie traces, on forwarde vers le backend Tempo sur l’endpoint tempo-distributor.tracing:4317 (format gRPC).

L’intérêt principal d’OTLP est leur enrichissement mutuel entre métriques, logs et traces, permettant une corrélation parfaite. Ce format est en revanche spécifique par application les supportant, vu qu’il s’agit d’un nouveau protocole en mode push. Il nous servira notamment pour OpenTelemetry plus tard.

Plus qu’à terraform apply pour déployer Alloy. Vérifer avec kgp -n telemetry que tout est bien déployé. Puis allez faire un tour sur https://alloy.dev.ohmytalos.io pour vérifier l’état des composants de collecte.

Alloy Components

Alloy fourni également un graphe de flux des données collectées.

Alloy Graph

Et voilà les logs devraient commencer à arriver dans loki très rapidement. Traefik étant déjà configuré à la section des ingress pour envoyer les traces, vous devriez aussi voir les premières traces arriver dans Tempo.

Visualisation des logs

Pour visualiser tout ça, allez dans la section Drilldown de Grafana, section logs pour avoir un aperçu rapide des logs des principaux composants.

Drilldown logs

Naviguer dans les logs de Traefik :

Drilldown Traefik logs

Grâce au derivedField configuré dans le datasource Loki, vous pouvez cliquer sur le TraceId dans les logs pour accéder directement à la trace correspondante dans Tempo.

Drilldown logs to trace

En dépliant les spans, vous y retrouverez les fameux liens créés dans la datasource de tempo, via les paramètres tracesToLogsV2 et tracesToMetrics.

Corrélation des logs sur le span sélectionné :

Drilldown trace to logs

Corrélation des métriques personnalisées sur le span sélectionné :

Drilldown trace to metrics

Vous pouvez créer autant de métriques personalisées que vous souhaitez. Il vous suffira de les ajouter dans la section queries du paramètre tracesToMetrics du datasource Tempo.

Visualisation des traces

Grafana fourni également une vue complète des traces :

Drilldown traces

Une vue par graphe des flux de services est aussi disponible, enrichie par les métriques personnalisées issues des traces :

Explore Service Graph

Conclusion

On est bon pour la mise en place des collecteurs et agrégateurs de logs et traces. Nous verrons plus tard au travers d’une application réelle comment l’intégrer à travers ces outils. Il est temps d’installer fluxcd pour le déploiement automatique de nos applications, c’est parti.