MFormations
Modern DevOps Engineering

Chapitre 10

10 - Logging

10 - Logging

Chapitre 10 : Logging

10.1 Introduction au Logging

10.1.1 Pourquoi le logging est essentiel

Le logging est le deuxieme pilier de l'observabilite. Il permet de :

  • Deboguer les applications en production
  • Auditer les acces et les actions
  • Detecter les anomalies et les incidents de securite
  • Analyser les tendances et les patterns d'utilisation
  • Satisfaire aux exigences de conformite (RGPD, SOC 2)

10.1.2 Logging structure vs non-structure

Log non-structure :

2024-01-15 10:30:45 User alice logged in from 192.168.1.1

Log structure (JSON) :

{
  "timestamp": "2024-01-15T10:30:45.000Z",
  "level": "INFO",
  "logger": "auth.service",
  "message": "User logged in",
  "user": "alice",
  "ip": "192.168.1.1",
  "duration_ms": 45,
  "correlation_id": "abc-123-def"
}

Log structure (logfmt) :

level=INFO ts=2024-01-15T10:30:45Z logger=auth.service msg="User logged in" user=alice ip=192.168.1.1 duration_ms=45 correlation_id=abc-123-def

10.1.3 Niveaux de log

NiveauValeurUtilisation
TRACE0Debugging tres detaille
DEBUG1Informations de debugging
INFO2Evenements normaux
WARN3Avertissements non critiques
ERROR4Erreurs necessitant action
FATAL5Erreurs fatales, arret du service

Bonnes pratiques :

  • Ne pas logger en DEBUG en production par defaut
  • Utiliser INFO pour les evenements metier importants
  • WARN pour les situations anormales mais recuperables
  • ERROR pour les erreurs necessitant une intervention humaine

10.1.4 Correlation ID

Le correlation ID (ou trace ID) permet de suivre une requete a travers tous les services :

Frontend                   API                    Payment
    │                        │                       │
    ├─ correlation_id: abc ──>                       │
    │                        ├─ correlation_id: abc ─>
    │                        │                       │
    │                        │                       │
    │                        │                       │
    │                        │                       │

Implementation en Python :

import uuid
from flask import Flask, request

app = Flask(__name__)

@app.before_request
def ensure_correlation_id():
    correlation_id = request.headers.get('X-Correlation-ID')
    if not correlation_id:
        correlation_id = str(uuid.uuid4())
    request.correlation_id = correlation_id

@app.route('/api/users')
def get_users():
    app.logger.info("Fetching users", extra={
        'correlation_id': request.correlation_id
    })
    return {"users": ["alice", "bob"]}

10.2 ELK Stack

10.2.1 Architecture ELK

L'ELK Stack se compose de :

  • Elasticsearch : Moteur de recherche et d'analyse distribue
  • Logstash : Pipeline de collecte et de transformation
  • Kibana : Interface de visualisation et d'exploration

Architecture :

Application ──> Filebeat ──> Logstash ──> Elasticsearch ──> Kibana
                                     │
                                Filter/Transform

10.2.2 Elasticsearch

Elasticsearch est un moteur de recherche distribue base sur Apache Lucene.

Concepts cles :

  • Index : Collection de documents similaires (equivalent a une table)
  • Document : Unite de donnees stockee au format JSON
  • Shard : Partition d'un index (distribue sur plusieurs nœuds)
  • Replica : Copie d'un shard (haute disponibilite)

Exemple d'index mapping :

PUT /logs-app
{
  "mappings": {
    "properties": {
      "@timestamp": { "type": "date" },
      "level": { "type": "keyword" },
      "message": { "type": "text" },
      "service": { "type": "keyword" },
      "correlation_id": { "type": "keyword" },
      "duration_ms": { "type": "integer" },
      "user": { "type": "keyword" }
    }
  },
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 2
  }
}

10.2.3 Logstash

Logstash est un pipeline de traitement de donnees :

input {
  beats {
    port => 5044
  }
}

filter {
  grok {
    match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:message}" }
  }
  
  date {
    match => ["timestamp", "ISO8601"]
    target => "@timestamp"
  }
  
  mutate {
    remove_field => ["timestamp"]
  }
  
  if [level] == "ERROR" {
    elasticsearch {
      hosts => ["localhost:9200"]
      index => "logs-app-error-%{+YYYY.MM.dd}"
    }
  }
}

output {
  elasticsearch {
    hosts => ["localhost:9200"]
    index => "logs-app-%{+YYYY.MM.dd}"
  }
}

10.2.4 Filebeat

Filebeat est un collecteur de logs leger :

filebeat.inputs:
  - type: container
    paths:
      - /var/log/containers/*.log

output.logstash:
  hosts: ["logstash:5044"]

logging.level: info
logging.to_files: true
logging.files:
  path: /var/log/filebeat
  name: filebeat.log
  keepfiles: 7

10.2.5 Kibana

Kibana permet d'explorer et visualiser les donnees Elasticsearch.

Fonctionnalites cles :

  • Discover : Exploration des logs en temps reel
  • Visualize : Creation de graphiques et tableaux
  • Dashboard : Assemblage de visualisations
  • Canvas : Presentations personnalisables
  • Machine Learning : Detection d'anomalies

Exemple de requete Kibana (KQL) :

level: ERROR AND service: payment AND duration_ms > 1000

10.3 Loki + Promtail

10.3.1 Architecture Loki

Loki est un systeme de logging horizontalement scalable, optimise pour Kubernetes :

Pods ──> Promtail ──> Loki ──> Grafana
                  │
             Labels (pod, namespace, container)

Contrairement a ELK, Loki :

  • N'indexe pas le contenu des logs, seulement les labels
  • Est beaucoup plus leger en ressources
  • S'integre nativement avec Grafana
  • Utilise le meme modele de labels que Prometheus

10.3.2 Promtail

Promtail est l'agent de collecte pour Loki :

# promtail-config.yaml
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: kubernetes-pods
    kubernetes_sd_configs:
      - role: pod
    pipeline_stages:
      - cri: {}
      - drop:
          expression: ".*healthcheck.*"
      - multiline:
          firstline: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'
      - labels:
          namespace:
            source_labels: ["__meta_kubernetes_namespace"]
          pod:
            source_labels: ["__meta_kubernetes_pod_name"]
          container:
            source_labels: ["__meta_kubernetes_container_name"]
      - static_labels:
          cluster: production-eu-west-1

10.3.3 Loki Configuration

# loki-config.yaml
auth_enabled: false

server:
  http_listen_port: 3100

ingester:
  lifecycler:
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
  chunk_idle_period: 1h
  chunk_retain_period: 30s
  max_transfer_retries: 0

schema_config:
  configs:
    - from: 2020-10-24
      store: boltdb-shipper
      object_store: filesystem
      schema: v11
      index:
        prefix: index_
        period: 24h

storage_config:
  boltdb_shipper:
    active_index_directory: /loki/boltdb-shipper-active
    cache_location: /loki/boltdb-shipper-cache
    cache_ttl: 24h
    shared_store: filesystem
  filesystem:
    directory: /loki/chunks

limits_config:
  enforce_metric_name: false
  reject_old_samples: true
  reject_old_samples_max_age: 168h
  ingestion_rate_mb: 10
  ingestion_burst_size_mb: 20

10.3.4 LogQL

LogQL est le langage de requete de Loki, inspire de PromQL :

# Tous les logs d'un namespace
{namespace="production"}

# Logs avec filtre sur le message
{namespace="production"} |= "ERROR"

# Logs avec expression reguliere
{service="api"} |~ "status=\d{3}"

# Logs avec exclusion
{app="frontend"} != "healthcheck"

# Aggregation temporelle
rate({namespace="production"} |= "ERROR" [5m])

# Top 5 des endpoints les plus lents
topk(5, max_over_time({job="api"} | json | unwrap duration_ms [1h]))

10.4 Fluentd et Fluent Bit

10.4.1 Fluentd

Fluentd est un collecteur de donnees open-source :

# fluentd-config.conf
<source>
  @type tail
  path /var/log/containers/*.log
  pos_file /var/log/fluentd-containers.log.pos
  tag kubernetes.*
  format json
  read_from_head true
</source>

<filter kubernetes.**>
  @type kubernetes_metadata
</filter>

<match kubernetes.var.log.containers.**>
  @type elasticsearch
  host "#{ENV['ES_HOST']}"
  port 9200
  logstash_format true
  logstash_prefix fluentd
  flush_interval 5s
</match>

10.4.2 Fluent Bit

Fluent Bit est une version ultra-leger de Fluentd (C vs Ruby) :

# fluent-bit-config.conf
[SERVICE]
    flush        1
    daemon       Off
    log_level    info
    parsers_file parsers.conf

[INPUT]
    Name              tail
    Path              /var/log/containers/*.log
    Parser            docker
    Tag               kube.*
    Mem_Buf_Limit     50MB
    Skip_Long_Lines   On

[FILTER]
    Name                kubernetes
    Match               kube.*
    Kube_URL            https://kubernetes.default.svc:443
    Kube_CA_File        /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
    Kube_Token_File     /var/run/secrets/kubernetes.io/serviceaccount/token
    Merge_Log           On
    Annotations         Off

[OUTPUT]
    Name          es
    Match         *
    Host          ${ES_HOST}
    Port          9200
    Index         k8s-logs
    Type          _doc
    Logstash_Format On
    Logstash_Prefix k8s
    Retry_Limit   6

10.4.3 Comparaison

CritereFluentdFluent Bit
LangageRuby + CC
Memoire~40MB~650KB
Debit~10K events/s~100K events/s
Plugins1000+100+
ConfigurationRuby DSLINI-like
Cas d'usageCollecte complexeCollecte legere

10.5 Bonnes Pratiques

10.5.1 Rotation des logs

La rotation des logs evite de saturer le disque :

# logrotate.conf
/var/log/application/*.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 0640 app app
    sharedscripts
    postrotate
        /usr/bin/systemctl reload app > /dev/null 2>&1 || true
    endscript
}

10.5.2 Centralisation des logs

Architecture de logging centralise :

                           ┌─> ELK (Analytics)
Application ──> Kafka ─────┼─> Loki (Kubernetes)
                           └─> S3 (Archive)
# docker-compose logging stack
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:latest
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181

  kafka:
    image: confluentinc/cp-kafka:latest
    depends_on:
      - zookeeper
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092

  logstash:
    image: docker.elastic.co/logstash/logstash:8.11.0
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf

10.5.3 GDPR et conformite

Principes GDPR applicables au logging :

  1. Minimisation : Ne logger que les donnees necessaires
  2. Limitation de conservation : Definir des periodes de retention
  3. Pseudonymisation : Masquer les donnees personnelles
  4. Droit a l'effacement : P pouvoir supprimer les logs d'un utilisateur

Exemple de filtre GDPR Logstash :

filter {
  # Masquer les emails
  mutate {
    gsub => ["message", "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", "***@***"]
  }
  
  # Masquer les IPs (pseudonymisation)
  mutate {
    gsub => ["message", "\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b", "XXX.XXX.XXX.XXX"]
  }
  
  # Masquer les numeros de carte bancaire
  mutate {
    gsub => ["message", "\b\d{16}\b", "****-****-****-****"]
  }
}

Politique de retention :

# Politique ILM Elasticsearch
PUT _ilm/policy/logs-policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "50GB",
            "max_age": "30d"
          }
        }
      },
      "warm": {
        "min_age": "30d",
        "actions": {
          "shrink": { "number_of_shards": 1 },
          "forcemerge": { "max_num_segments": 1 }
        }
      },
      "delete": {
        "min_age": "90d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

10.5.4 Correlation ID en pratique

# middleware_correlation.py
import uuid
import logging
from flask import Flask, request, g

app = Flask(__name__)

class CorrelationFilter(logging.Filter):
    def filter(self, record):
        record.correlation_id = getattr(g, 'correlation_id', 'N/A')
        return True

@app.before_request
def set_correlation_id():
    g.correlation_id = request.headers.get(
        'X-Correlation-ID', 
        str(uuid.uuid4())
    )

@app.after_request
def add_correlation_header(response):
    response.headers['X-Correlation-ID'] = g.correlation_id
    return response

# Configuration du logger
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
    '{"timestamp": "%(asctime)s", "level": "%(levelname)s", '
    '"correlation_id": "%(correlation_id)s", '
    '"message": "%(message)s"}'
))
handler.addFilter(CorrelationFilter())
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)

@app.route('/api/users')
def get_users():
    app.logger.info("Fetching users list")
    return {"users": ["alice", "bob"]}

Resume

  • Le logging structure (JSON/logfmt) permet une analyse automatisee
  • L'ELK Stack est la solution historique de centralisation de logs
  • Loki offre une alternative cloud-native optimisee pour Kubernetes
  • Fluentd/Fluent Bit sont des collecteurs flexibles et performants
  • Les correlation IDs sont essentiels pour le debugging distribue
  • La rotation et la retention des logs sont cruciales pour la production
  • Le GDPR impose des regles strictes sur les donnees dans les logs