================================================================================
[OK] ELK STACK EN ÉQUIPE DE 3 DÉVELOPPEURS - APPLICATION FLASK (GUIDE ULTRA-DÉTAILLÉ)
================================================================================

Ce guide couvre un cas réel et concret: la même équipe de 3 développeurs Python
qui travaillent sur l'application Flask "TaskManager API" et utilisent la stack ELK
(Elasticsearch + Logstash + Kibana) pour centraliser, analyser et visualiser
les logs de leur application collectivement.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
QU'EST-CE QUE LA STACK ELK?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

La stack ELK est composée de 3 outils open-source maintenus par Elastic:

  E - Elasticsearch  -> Moteur de recherche et stockage des logs
  L - Logstash       -> Pipeline d'ingestion et transformation des logs
  K - Kibana         -> Interface web de visualisation et dashboards

  Flux de données:
  ┌─────────────────────────────────────────────────────────────────┐
  │  Application Flask                                              │
  │  (génère des logs)                                             │
  │         │                                                       │
  │         [BLACK_DOWN-POINTING_TRIANGLE]                                                       │
  │  Logstash (collecte + transforme)                              │
  │         │                                                       │
  │         [BLACK_DOWN-POINTING_TRIANGLE]                                                       │
  │  Elasticsearch (indexe + stocke)                               │
  │         │                                                       │
  │         [BLACK_DOWN-POINTING_TRIANGLE]                                                       │
  │  Kibana (visualise + alerte)                                   │
  └─────────────────────────────────────────────────────────────────┘

  Dans notre cas concret, on utilisera aussi:
  - python-logstash : bibliothèque Python pour envoyer les logs directement
  - Filebeat        : agent léger qui lit les fichiers de logs (alternative à Logstash)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRÉSENTATION DE L'ÉQUIPE ET DU PROJET (SUITE DU GUIDE SONARQUBE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

L'équipe (identique au guide SonarQube):
  - Alice  -> Lead développeuse (aussi admin ELK)
  - Bob    -> Développeur backend
  - Claire -> Développeuse backend + tests

Le projet:
  - Nom: "TaskManager API"
  - Stack: Flask + SQLAlchemy + PostgreSQL
  - Versionning: Git + GitHub
  - Dépôt: github.com/equipe/taskmanager

Ce que l'équipe veut logger:
  - Toutes les requêtes HTTP (route, méthode, statut, durée)
  - Les erreurs et exceptions
  - Les opérations CRUD sur les tâches
  - Les performances des requêtes SQL
  - Les tentatives d'accès suspectes (ex: paramètres invalides répétés)

Structure du projet mise à jour:
  taskmanager/
  ├── app/
  │   ├── __init__.py          <- Modifié pour intégrer les logs
  │   ├── models.py
  │   ├── routes.py
  │   ├── utils.py
  │   └── logger.py            <- NOUVEAU: configuration centralisée du logging
  ├── tests/
  │   ├── conftest.py
  │   ├── test_routes.py
  │   └── test_models.py
  ├── docker/
  │   ├── docker-compose.elk.yml  <- NOUVEAU: stack ELK en Docker
  │   ├── logstash/
  │   │   ├── pipeline/
  │   │   │   └── taskmanager.conf  <- NOUVEAU: config pipeline Logstash
  │   │   └── logstash.yml
  │   └── filebeat/
  │       └── filebeat.yml      <- NOUVEAU: config Filebeat
  ├── logs/                     <- NOUVEAU: dossier des logs applicatifs
  │   └── .gitkeep
  ├── requirements.txt          <- Mis à jour avec python-logstash
  └── sonar-project.properties


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 1 - MISE EN PLACE PAR ALICE (ADMIN / LEAD DEV)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Alice est chargée de mettre en place la stack ELK pour toute l'équipe.
Elle fait cela une seule fois. Bob et Claire n'ont qu'à suivre la PARTIE 2.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.1 - Alice crée le fichier docker-compose pour la stack ELK
────────────────────────────────────────────────────────────────────────────────

Alice crée le fichier docker/docker-compose.elk.yml:

  COMMANDES TERMINAL:
  ───────────────────
  cd taskmanager/
  mkdir -p docker/logstash/pipeline docker/filebeat
  nano docker/docker-compose.elk.yml

  CONTENU DU FICHIER docker/docker-compose.elk.yml:
  ──────────────────────────────────────────────────

  version: '3.8'

  services:

    # ─────────────────────────────────────────────
    # ELASTICSEARCH - Moteur de recherche et stockage
    # ─────────────────────────────────────────────
    elasticsearch:
      image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
      container_name: elasticsearch
      environment:
        - node.name=elasticsearch
        - cluster.name=taskmanager-cluster
        - discovery.type=single-node         # Mode solo (pas de cluster)
        - bootstrap.memory_lock=true
        - xpack.security.enabled=false       # Désactivé pour simplifier (dev only!)
        - "ES_JAVA_OPTS=-Xms512m -Xmx512m"  # Limiter la mémoire JVM
      ulimits:
        memlock:
          soft: -1
          hard: -1
      volumes:
        - elasticsearch_data:/usr/share/elasticsearch/data
      ports:
        - "9200:9200"    # API REST Elasticsearch
        - "9300:9300"    # Communication inter-nœuds
      networks:
        - elk
      healthcheck:
        test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health | grep -q '\"status\":\"green\\|yellow\"'"]
        interval: 30s
        timeout: 10s
        retries: 5

    # ─────────────────────────────────────────────
    # LOGSTASH - Pipeline d'ingestion des logs
    # ─────────────────────────────────────────────
    logstash:
      image: docker.elastic.co/logstash/logstash:8.11.0
      container_name: logstash
      volumes:
        - ./logstash/pipeline:/usr/share/logstash/pipeline:ro
        - ./logstash/logstash.yml:/usr/share/logstash/config/logstash.yml:ro
        - ../logs:/logs:ro                   # Dossier logs de l'app Flask
      ports:
        - "5000:5000/tcp"    # Entrée TCP (python-logstash)
        - "5000:5000/udp"    # Entrée UDP
        - "5044:5044"        # Entrée Beats (Filebeat)
        - "9600:9600"        # API monitoring Logstash
      environment:
        LS_JAVA_OPTS: "-Xmx256m -Xms256m"
      networks:
        - elk
      depends_on:
        elasticsearch:
          condition: service_healthy

    # ─────────────────────────────────────────────
    # KIBANA - Interface de visualisation
    # ─────────────────────────────────────────────
    kibana:
      image: docker.elastic.co/kibana/kibana:8.11.0
      container_name: kibana
      ports:
        - "5601:5601"    # Interface web Kibana
      environment:
        ELASTICSEARCH_URL: http://elasticsearch:9200
        ELASTICSEARCH_HOSTS: '["http://elasticsearch:9200"]'
      networks:
        - elk
      depends_on:
        elasticsearch:
          condition: service_healthy

    # ─────────────────────────────────────────────
    # FILEBEAT - Agent de collecte de fichiers logs
    # ─────────────────────────────────────────────
    filebeat:
      image: docker.elastic.co/beats/filebeat:8.11.0
      container_name: filebeat
      user: root
      volumes:
        - ./filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
        - ../logs:/logs:ro
      networks:
        - elk
      depends_on:
        - logstash

  volumes:
    elasticsearch_data:
      driver: local

  networks:
    elk:
      driver: bridge

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.2 - Alice crée la configuration Logstash
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  nano docker/logstash/logstash.yml

  CONTENU:
  ────────
  http.host: "0.0.0.0"
  xpack.monitoring.elasticsearch.hosts: ["http://elasticsearch:9200"]

  COMMANDE:
  ─────────
  nano docker/logstash/pipeline/taskmanager.conf

  CONTENU DU FICHIER taskmanager.conf:
  ─────────────────────────────────────

  # ══════════════════════════════════════════════════════════
  # PIPELINE LOGSTASH - TaskManager API
  # ══════════════════════════════════════════════════════════

  # ─────────────────────────────────────────────
  # SECTION INPUT: D'où viennent les logs?
  # ─────────────────────────────────────────────
  input {

    # Source 1: Logs envoyés directement via python-logstash (TCP)
    tcp {
      port => 5000
      codec => json_lines    # Les logs sont au format JSON
      tags => ["python-app", "direct"]
    }

    # Source 2: Logs lus depuis les fichiers par Filebeat
    beats {
      port => 5044
      tags => ["filebeat"]
    }

    # Source 3: Fichiers de logs JSON (lecture directe dans le conteneur)
    file {
      path => "/logs/taskmanager.json"
      start_position => "beginning"
      sincedb_path => "/dev/null"   # Pour le dev: relit tout à chaque démarrage
      codec => json
      tags => ["file-input"]
    }
  }

  # ─────────────────────────────────────────────
  # SECTION FILTER: Transformation et enrichissement
  # ─────────────────────────────────────────────
  filter {

    # Supprimer les champs internes Logstash (non utiles)
    mutate {
      remove_field => ["@version", "host", "port"]
    }

    # Enrichissement: Parser le champ "message" s'il existe
    if [message] and [message] =~ /^\{/ {
      json {
        source => "message"
        target => "parsed"
        remove_field => ["message"]
      }
    }

    # Normalisation des niveaux de log
    if [level] {
      mutate {
        uppercase => ["level"]
      }
    }

    # Enrichissement: Extraire la durée en millisecondes si présente
    if [duration_ms] {
      mutate {
        convert => { "duration_ms" => "float" }
      }
    }

    # Enrichissement: Ajouter un champ "slow_request" si durée > 500ms
    if [duration_ms] and [duration_ms] > 500 {
      mutate {
        add_field => { "slow_request" => true }
        add_tag   => ["slow"]
      }
    }

    # Enrichissement: Marquer les erreurs HTTP (4xx, 5xx)
    if [http_status] {
      mutate {
        convert => { "http_status" => "integer" }
      }
      if [http_status] >= 400 {
        mutate {
          add_tag => ["http_error"]
        }
      }
      if [http_status] >= 500 {
        mutate {
          add_tag => ["server_error"]
        }
      }
    }

    # Ajouter le nom de l'application et l'environnement
    mutate {
      add_field => {
        "app_name"    => "taskmanager-api"
        "environment" => "${APP_ENV:development}"
      }
    }

    # Gestion des timestamps
    date {
      match => ["timestamp", "ISO8601"]
      target => "@timestamp"
      remove_field => ["timestamp"]
    }
  }

  # ─────────────────────────────────────────────
  # SECTION OUTPUT: Où envoyer les logs traités?
  # ─────────────────────────────────────────────
  output {

    # Sortie principale: Elasticsearch
    elasticsearch {
      hosts => ["http://elasticsearch:9200"]
      index => "taskmanager-logs-%{+YYYY.MM.dd}"
      # Exemple d'index: taskmanager-logs-2024.01.15
    }

    # Sortie debug: Afficher dans la console Logstash (utile en dev)
    # stdout {
    #   codec => rubydebug
    # }
  }

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.3 - Alice crée la configuration Filebeat
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  nano docker/filebeat/filebeat.yml

  CONTENU:
  ────────

  filebeat.inputs:
    - type: log
      enabled: true
      paths:
        - /logs/taskmanager.json     # Fichier de log JSON de l'app Flask
      json.keys_under_root: true     # Met les clés JSON à la racine
      json.add_error_key: true
      multiline:                     # Gérer les exceptions Python multi-lignes
        pattern: '^[[:space:]]+(at|\.{3})\b|^Caused by:'
        negate: false
        match: after

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

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

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.4 - Alice lance la stack ELK
────────────────────────────────────────────────────────────────────────────────

  COMMANDES TERMINAL:
  ───────────────────
  cd taskmanager/docker/

  # Lancer tous les services ELK en arrière-plan
  docker-compose -f docker-compose.elk.yml up -d

  # Vérifier que les conteneurs démarrent:
  docker-compose -f docker-compose.elk.yml ps

  SORTIE ATTENDUE (après ~2 minutes):
  ────────────────────────────────────
  Name              Command               State                Ports
  ────────────────────────────────────────────────────────────────────────────
  elasticsearch     /bin/tini -- /usr/local/bin   Up    0.0.0.0:9200->9200/tcp
  kibana            /bin/tini -- /usr/local/bin   Up    0.0.0.0:5601->5601/tcp
  logstash          /usr/local/bin/docker-ent...  Up    0.0.0.0:5000->5000/tcp
  filebeat          /usr/local/bin/docker-ent...  Up

  # Vérifier qu'Elasticsearch répond:
  curl http://localhost:9200

  RÉSULTAT ATTENDU:
  ─────────────────
  {
    "name" : "elasticsearch",
    "cluster_name" : "taskmanager-cluster",
    "version" : {
      "number" : "8.11.0",
      ...
    },
    "tagline" : "You Know, for Search"
  }

  # Vérifier la santé du cluster:
  curl http://localhost:9200/_cluster/health?pretty

  RÉSULTAT ATTENDU:
  ─────────────────
  {
    "cluster_name" : "taskmanager-cluster",
    "status" : "green",         <- ou "yellow" en single-node, c'est normal
    "number_of_nodes" : 1,
    "number_of_data_nodes" : 1,
    ...
  }

  # Voir les logs en temps réel si problème:
  docker-compose -f docker-compose.elk.yml logs -f elasticsearch
  docker-compose -f docker-compose.elk.yml logs -f logstash

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.5 - Alice configure le logging Python dans l'application Flask
────────────────────────────────────────────────────────────────────────────────

Alice installe les dépendances Python nécessaires:

  COMMANDES TERMINAL:
  ───────────────────
  cd taskmanager/
  source venv/bin/activate
  pip install python-logstash python-json-logger

  # Mettre à jour requirements.txt:
  echo "python-logstash==0.4.8" >> requirements.txt
  echo "python-json-logger==2.0.7" >> requirements.txt

  # Créer le dossier des logs:
  mkdir -p logs
  touch logs/.gitkeep

Alice crée le fichier de configuration du logging:

  COMMANDE:
  ─────────
  nano app/logger.py

  CONTENU COMPLET DE app/logger.py:
  ───────────────────────────────────

  """
  Module de configuration du logging pour TaskManager API.
  Configure deux handlers:
  1. Fichier JSON local (taskmanager.json)
  2. Logstash TCP (envoi direct vers ELK)
  """

  import logging
  import os
  import time
  import traceback
  from functools import wraps
  from pythonjsonlogger import jsonlogger
  import logstash

  # ─────────────────────────────────────────────
  # CONSTANTES DE CONFIGURATION
  # ─────────────────────────────────────────────
  LOGSTASH_HOST = os.environ.get('LOGSTASH_HOST', 'localhost')
  LOGSTASH_PORT = int(os.environ.get('LOGSTASH_PORT', 5000))
  LOG_LEVEL     = os.environ.get('LOG_LEVEL', 'INFO').upper()
  APP_ENV       = os.environ.get('APP_ENV', 'development')
  LOG_FILE      = os.environ.get('LOG_FILE', 'logs/taskmanager.json')

  # ─────────────────────────────────────────────
  # FORMATTER JSON PERSONNALISÉ
  # ─────────────────────────────────────────────
  class TaskManagerFormatter(jsonlogger.JsonFormatter):
      """
      Formatteur JSON qui ajoute des champs contextuels à chaque log.
      Format final dans Elasticsearch:
      {
        "@timestamp": "2024-01-15T10:30:00Z",
        "level": "INFO",
        "message": "Tâche créée",
        "app": "taskmanager-api",
        "env": "development",
        "module": "routes",
        "func_name": "create_task",
        ... (champs métier spécifiques)
      }
      """

      def add_fields(self, log_record, record, message_dict):
          super().add_fields(log_record, record, message_dict)

          # Champs systèmes standard
          log_record['level']      = record.levelname
          log_record['module']     = record.module
          log_record['func_name']  = record.funcName
          log_record['line']       = record.lineno
          log_record['app']        = 'taskmanager-api'
          log_record['env']        = APP_ENV

          # Ajouter le traceback si exception
          if record.exc_info:
              log_record['exception'] = self.formatException(record.exc_info)
              log_record['traceback'] = traceback.format_exc()


  # ─────────────────────────────────────────────
  # FONCTION SETUP LOGGER
  # ─────────────────────────────────────────────
  def setup_logger(name='taskmanager'):
      """
      Configure et retourne un logger avec handlers fichier + Logstash.

      Args:
          name: Nom du logger (par défaut 'taskmanager')

      Returns:
          logging.Logger: Logger configuré et prêt à l'emploi

      Usage:
          from app.logger import setup_logger
          logger = setup_logger()
          logger.info("Message", extra={"user_id": 42, "action": "create"})
      """

      logger = logging.getLogger(name)

      # Éviter la duplication des handlers si appelé plusieurs fois
      if logger.handlers:
          return logger

      logger.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))

      formatter = TaskManagerFormatter(
          fmt='%(asctime)s %(levelname)s %(name)s %(message)s',
          datefmt='%Y-%m-%dT%H:%M:%S'
      )

      # ── Handler 1: Fichier JSON local ─────────────────────────────────
      try:
          os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
          file_handler = logging.FileHandler(LOG_FILE)
          file_handler.setFormatter(formatter)
          file_handler.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
          logger.addHandler(file_handler)
      except (IOError, OSError) as e:
          print(f"[WARNING] Impossible de créer le fichier de log: {e}")

      # ── Handler 2: Logstash TCP ────────────────────────────────────────
      try:
          logstash_handler = logstash.TCPLogstashHandler(
              LOGSTASH_HOST,
              LOGSTASH_PORT,
              version=1
          )
          logstash_handler.setLevel(getattr(logging, LOG_LEVEL, logging.INFO))
          logger.addHandler(logstash_handler)
          logger.info(
              "Logstash handler initialisé",
              extra={"logstash_host": LOGSTASH_HOST, "logstash_port": LOGSTASH_PORT}
          )
      except Exception as e:
          # Si Logstash n'est pas disponible, on log quand même en fichier
          print(f"[WARNING] Logstash non disponible ({LOGSTASH_HOST}:{LOGSTASH_PORT}): {e}")
          print("[WARNING] Les logs seront écrits uniquement dans le fichier local.")

      # ── Handler 3: Console (en développement seulement) ───────────────
      if APP_ENV == 'development':
          console_handler = logging.StreamHandler()
          console_handler.setFormatter(
              logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s')
          )
          console_handler.setLevel(logging.DEBUG)
          logger.addHandler(console_handler)

      return logger


  # ─────────────────────────────────────────────
  # DÉCORATEUR POUR LOGGER LES REQUÊTES HTTP
  # ─────────────────────────────────────────────
  def log_request(logger):
      """
      Décorateur Flask qui logue automatiquement chaque appel d'une route.
      Enregistre: méthode HTTP, route, durée, statut de réponse.

      Usage dans routes.py:
          @tasks_bp.route('/tasks', methods=['GET'])
          @log_request(logger)
          def get_tasks():
              ...
      """
      def decorator(func):
          @wraps(func)
          def wrapper(*args, **kwargs):
              from flask import request, g
              start_time = time.time()

              try:
                  response = func(*args, **kwargs)
                  duration_ms = round((time.time() - start_time) * 1000, 2)

                  # Récupérer le code de statut HTTP
                  if hasattr(response, 'status_code'):
                      status_code = response.status_code
                  elif isinstance(response, tuple):
                      status_code = response[1] if len(response) > 1 else 200
                  else:
                      status_code = 200

                  logger.info(
                      f"HTTP {request.method} {request.path}",
                      extra={
                          "event_type":   "http_request",
                          "http_method":  request.method,
                          "http_path":    request.path,
                          "http_status":  status_code,
                          "duration_ms":  duration_ms,
                          "user_agent":   request.headers.get('User-Agent', ''),
                          "remote_addr":  request.remote_addr,
                      }
                  )
                  return response

              except Exception as exc:
                  duration_ms = round((time.time() - start_time) * 1000, 2)
                  logger.error(
                      f"Erreur HTTP {request.method} {request.path}: {str(exc)}",
                      exc_info=True,
                      extra={
                          "event_type":   "http_error",
                          "http_method":  request.method,
                          "http_path":    request.path,
                          "http_status":  500,
                          "duration_ms":  duration_ms,
                          "error":        str(exc),
                      }
                  )
                  raise

          return wrapper
      return decorator

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.6 - Alice intègre le logging dans l'application Flask
────────────────────────────────────────────────────────────────────────────────

Alice modifie app/__init__.py pour initialiser le logger au démarrage:

  CONTENU MIS À JOUR DE app/__init__.py:
  ───────────────────────────────────────

  from flask import Flask, jsonify, request
  from flask_sqlalchemy import SQLAlchemy
  import time

  db = SQLAlchemy()

  def create_app(config=None):
      app = Flask(__name__)
      app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tasks.db'
      app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

      if config:
          app.config.update(config)

      db.init_app(app)

      # ── Initialisation du logger ───────────────────────────────────────
      from app.logger import setup_logger
      logger = setup_logger()
      app.logger_elk = logger   # Rendre le logger accessible depuis l'app

      # ── Middleware: Logger toutes les requêtes ─────────────────────────
      @app.before_request
      def before_request():
          """Enregistrer le timestamp de début de requête."""
          request._start_time = time.time()

      @app.after_request
      def after_request(response):
          """Logger chaque requête HTTP après traitement."""
          duration_ms = round((time.time() - request._start_time) * 1000, 2)

          logger.info(
              f"HTTP {request.method} {request.path} -> {response.status_code}",
              extra={
                  "event_type":   "http_request",
                  "http_method":  request.method,
                  "http_path":    request.path,
                  "http_status":  response.status_code,
                  "duration_ms":  duration_ms,
                  "remote_addr":  request.remote_addr,
                  "user_agent":   request.headers.get('User-Agent', '')[:200],
              }
          )
          return response

      @app.errorhandler(404)
      def not_found(error):
          logger.warning(
              f"Ressource non trouvée: {request.path}",
              extra={
                  "event_type": "http_404",
                  "http_path":  request.path,
                  "http_method": request.method,
              }
          )
          return jsonify({"error": "Not found"}), 404

      @app.errorhandler(500)
      def internal_error(error):
          logger.error(
              f"Erreur interne: {str(error)}",
              exc_info=True,
              extra={
                  "event_type": "http_500",
                  "http_path":  request.path,
              }
          )
          return jsonify({"error": "Internal server error"}), 500

      # ── Blueprints ─────────────────────────────────────────────────────
      from app.routes import tasks_bp
      app.register_blueprint(tasks_bp)

      with app.app_context():
          db.create_all()

      logger.info(
          "Application Flask démarrée",
          extra={"event_type": "app_start", "config": str(type(config))}
      )

      return app

Alice modifie app/routes.py pour ajouter des logs métier détaillés:

  CONTENU MIS À JOUR DE app/routes.py:
  ──────────────────────────────────────

  from flask import Blueprint, request, jsonify, abort, current_app
  from app import db
  from app.models import Task
  from app.logger import setup_logger

  tasks_bp = Blueprint('tasks', __name__)
  logger = setup_logger('taskmanager.routes')


  @tasks_bp.route('/tasks', methods=['GET'])
  def get_tasks():
      tasks = Task.query.all()
      logger.info(
          "Liste des tâches récupérée",
          extra={
              "event_type":  "task_list",
              "task_count":  len(tasks),
          }
      )
      return jsonify([t.to_dict() for t in tasks])


  @tasks_bp.route('/tasks', methods=['POST'])
  def create_task():
      data = request.get_json()
      if not data or 'title' not in data:
          logger.warning(
              "Tentative de création sans titre",
              extra={
                  "event_type": "task_create_error",
                  "payload":    str(data)[:200],
              }
          )
          abort(400, description="Title is required")

      task = Task(title=data['title'], description=data.get('description'))
      db.session.add(task)
      db.session.commit()

      logger.info(
          f"Tâche créée: '{task.title}'",
          extra={
              "event_type": "task_created",
              "task_id":    task.id,
              "task_title": task.title,
          }
      )
      return jsonify(task.to_dict()), 201


  @tasks_bp.route('/tasks/<int:task_id>', methods=['GET'])
  def get_task(task_id):
      task = Task.query.get(task_id)
      if task is None:
          logger.warning(
              f"Tâche {task_id} introuvable",
              extra={"event_type": "task_not_found", "task_id": task_id}
          )
          abort(404, description="Task not found")

      logger.debug(
          f"Tâche {task_id} consultée",
          extra={"event_type": "task_read", "task_id": task_id}
      )
      return jsonify(task.to_dict())


  @tasks_bp.route('/tasks/<int:task_id>', methods=['PUT'])
  def update_task(task_id):
      task = Task.query.get(task_id)
      if task is None:
          logger.warning(
              f"Mise à jour impossible: tâche {task_id} introuvable",
              extra={"event_type": "task_update_not_found", "task_id": task_id}
          )
          abort(404, description="Task not found")

      data = request.get_json()
      if not data:
          abort(400, description="No data provided")

      old_done = task.done
      task.title       = data.get('title', task.title)
      task.description = data.get('description', task.description)
      task.done        = data.get('done', task.done)
      db.session.commit()

      # Log spécifique si une tâche est marquée comme terminée
      if not old_done and task.done:
          logger.info(
              f"Tâche {task_id} marquée comme terminée",
              extra={"event_type": "task_completed", "task_id": task_id, "task_title": task.title}
          )
      else:
          logger.info(
              f"Tâche {task_id} mise à jour",
              extra={"event_type": "task_updated", "task_id": task_id}
          )

      return jsonify(task.to_dict())


  @tasks_bp.route('/tasks/<int:task_id>', methods=['DELETE'])
  def delete_task(task_id):
      task = Task.query.get(task_id)
      if task is None:
          logger.warning(
              f"Suppression impossible: tâche {task_id} introuvable",
              extra={"event_type": "task_delete_not_found", "task_id": task_id}
          )
          abort(404, description="Task not found")

      title = task.title
      db.session.delete(task)
      db.session.commit()

      logger.info(
          f"Tâche supprimée: '{title}'",
          extra={"event_type": "task_deleted", "task_id": task_id, "task_title": title}
      )
      return jsonify({'message': 'Task deleted'}), 200

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.7 - Alice se connecte à Kibana et configure l'index pattern
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB Kibana - Connexion initiale:

  1. Alice ouvre son navigateur et va sur:
     http://localhost:5601

  2. Elle voit la page de bienvenue Kibana:
     ┌─────────────────────────────────────────────────┐
     │  [JAUNE] Kibana                                      │
     │                                                 │
     │  Welcome to Elastic!                           │
     │  Get started by ingesting your data.           │
     │                                                 │
     │  [  Explore on my own  ]                       │
     └─────────────────────────────────────────────────┘

  3. Elle clique "Explore on my own"

  4. Elle lance d'abord l'application Flask pour générer des logs:
     COMMANDE TERMINAL:
     ──────────────────
     cd taskmanager/
     source venv/bin/activate
     LOGSTASH_HOST=localhost LOGSTASH_PORT=5000 python run.py &

     # Générer quelques requêtes de test:
     curl http://localhost:5000/tasks
     curl -X POST http://localhost:5000/tasks \
          -H "Content-Type: application/json" \
          -d '{"title": "Test ELK", "description": "Vérifier les logs"}'
     curl http://localhost:5000/tasks/1
     curl http://localhost:5000/tasks/999    # -> 404 pour tester les erreurs

INTERFACE WEB Kibana - Créer l'index pattern:

  5. Dans Kibana, aller dans le menu hamburger (≡) > Management > Stack Management

  6. Dans la section "Kibana": cliquer "Data Views" (anciennement "Index Patterns")

  7. Cliquer "+ Create data view":
     ┌────────────────────────────────────────────────────────────┐
     │  Create data view                                          │
     │                                                            │
     │  Name*:                                                    │
     │  [TaskManager Logs                              ]          │
     │                                                            │
     │  Index pattern*:                                           │
     │  [taskmanager-logs-*                            ]          │
     │  -> Ce pattern correspond à tous les index journaliers      │
     │    Ex: taskmanager-logs-2024.01.15                        │
     │                                                            │
     │  Timestamp field:                                          │
     │  [@timestamp               [BLACK_DOWN-POINTING_TRIANGLE]]                             │
     │                                                            │
     │  Aperçu des index correspondants:                          │
     │  [OK] taskmanager-logs-2024.01.15  (1,247 documents)        │
     │                                                            │
     │  [    Save data view to Kibana    ]                        │
     └────────────────────────────────────────────────────────────┘

  8. Cliquer "Save data view to Kibana"

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.8 - Alice crée les dashboards pour l'équipe
────────────────────────────────────────────────────────────────────────────────

Alice crée 3 dashboards utiles pour toute l'équipe.

INTERFACE WEB Kibana - Dashboard 1: Vue générale:

  1. Menu ≡ > Analytics > Dashboards
  2. Cliquer "+ Create dashboard"
  3. Cliquer "Create visualization"

  ── Visualisation 1.1: Nombre de requêtes par heure ────────────────────────

  - Type de visualisation: Bar chart (histogramme)
  - Champ X-axis: @timestamp (auto)
  - Champ Y-axis: Count (nombre de documents)
  - Titre: "Requêtes par heure"
  Configurer dans le panneau de droite:
    Metrics > Vertical axis: Count
    Buckets > Horizontal axis: Date histogram
      Field: @timestamp
      Minimum interval: 1 hour
  -> Cliquer "Save and return"

  ── Visualisation 1.2: Répartition des codes HTTP ───────────────────────────

  - Type de visualisation: Pie chart (camembert)
  - Titre: "Codes HTTP"
  Configuration:
    Metrics > Slice size: Count
    Buckets > Split slices:
      Aggregation: Terms
      Field: http_status
      Size: 10
  -> Les tranches seront: 200, 201, 400, 404, 500
  -> Cliquer "Save and return"

  ── Visualisation 1.3: Top 5 routes les plus appelées ──────────────────────

  - Type de visualisation: Horizontal bar chart
  - Titre: "Top routes"
  Configuration:
    Metrics: Count
    Buckets: Terms on http_path.keyword (top 5)
  -> Cliquer "Save and return"

  ── Visualisation 1.4: Durée moyenne des requêtes ──────────────────────────

  - Type de visualisation: Metric (nombre affiché en grand)
  - Titre: "Durée moyenne (ms)"
  Configuration:
    Metrics > Average of duration_ms
  -> Cliquer "Save and return"

  4. Cliquer "Save" et nommer le dashboard:
     "TaskManager - Vue Générale"

INTERFACE WEB Kibana - Dashboard 2: Erreurs et alertes:

  1. Créer un nouveau dashboard nommé "TaskManager - Erreurs"

  ── Visualisation 2.1: Timeline des erreurs ─────────────────────────────────

  - Filtre KQL (Kibana Query Language) en haut: http_status >= 400
  - Type: Line chart (courbe)
  - X: @timestamp (1 heure)
  - Y: Count
  - Titre: "Erreurs HTTP dans le temps"

  ── Visualisation 2.2: Tableau des 10 dernières erreurs ─────────────────────

  - Type: Data table
  - Filtre: level: "ERROR" OR http_status >= 500
  - Colonnes: @timestamp, level, message, http_path, http_status
  - Titre: "Dernières erreurs"
  Configurer:
    Metrics: Count
    Buckets: Terms on message.keyword (top 10)

  ── Visualisation 2.3: Gauge de taux d'erreur ──────────────────────────────

  - Type: Gauge
  - Calcul: (count errors / count total) * 100
  - Seuils:
    Vert:  0-5%
    Jaune: 5-10%
    Rouge: >10%
  - Titre: "Taux d'erreur (%)"

INTERFACE WEB Kibana - Dashboard 3: Performances:

  1. Créer un nouveau dashboard nommé "TaskManager - Performances"

  ── Visualisation 3.1: Percentiles de durée ─────────────────────────────────

  - Type: Line chart
  - Y: Percentile of duration_ms (p50, p90, p99)
  - X: @timestamp
  - Titre: "Temps de réponse (p50 / p90 / p99)"
  Configuration:
    Ajouter 3 métriques Y:
    - Percentile 50 de duration_ms  (label: "Médiane")
    - Percentile 90 de duration_ms  (label: "P90")
    - Percentile 99 de duration_ms  (label: "P99")

  ── Visualisation 3.2: Requêtes lentes ──────────────────────────────────────

  - Filtre: slow_request: true
  - Type: Data table
  - Colonnes: @timestamp, http_path, http_method, duration_ms
  - Titre: "Requêtes lentes (>500ms)"

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.9 - Alice crée les comptes Kibana pour Bob et Claire
────────────────────────────────────────────────────────────────────────────────

NOTE: Dans la configuration docker-compose, la sécurité X-Pack est désactivée
(xpack.security.enabled=false) pour simplifier le développement.
En production, il faudrait l'activer et créer des utilisateurs via Elasticsearch.

Pour activer la gestion des utilisateurs en développement:

  COMMANDE (modification dans docker-compose.elk.yml):
  ─────────────────────────────────────────────────────
  # Dans la section elasticsearch, changer:
  - xpack.security.enabled=false
  # Par:
  - xpack.security.enabled=true
  - ELASTIC_PASSWORD=ElasticPass2024!

  # Et dans la section kibana, ajouter:
  - ELASTICSEARCH_USERNAME=kibana_system
  - ELASTICSEARCH_PASSWORD=KibanaPass2024!

INTERFACE WEB Kibana - Créer les utilisateurs (si sécurité activée):

  1. Menu ≡ > Management > Stack Management > Security > Users

  2. Cliquer "+ Create user" pour Bob:
     ┌─────────────────────────────────────────────────┐
     │  Create user                                    │
     │                                                 │
     │  Username*:   [bob                  ]           │
     │  Full name:   [Bob Dupont           ]           │
     │  Email:       [bob@equipe.com       ]           │
     │  Password*:   [BobKibana2024!       ]           │
     │                                                 │
     │  Roles:                                         │
     │  [kibana_user           [BLACK_DOWN-POINTING_TRIANGLE]] <- Ajouter           │
     │  [viewer                [BLACK_DOWN-POINTING_TRIANGLE]] <- Ajouter           │
     │                                                 │
     │  [   Create user   ]                            │
     └─────────────────────────────────────────────────┘

  3. Répéter pour Claire:
     Username: claire
     Password: ClaireKibana2024!
     Roles: kibana_user, viewer

  4. Alice garde le rôle "superuser" pour elle-même.

  RÉSUMÉ DES RÔLES:
  ─────────────────
  alice  -> superuser      (tout faire: créer dashboards, gérer index, etc.)
  bob    -> kibana_user    (voir et créer ses propres dashboards)
  claire -> viewer         (voir les dashboards partagés, pas de création)


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 2 - CONFIGURATION POSTE DE BOB ET CLAIRE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Bob et Claire reçoivent d'Alice:
  - L'URL de Kibana:       http://[IP_D_ALICE]:5601
  - L'URL d'Elasticsearch: http://[IP_D_ALICE]:9200
  - Leurs identifiants Kibana
  - Les instructions de configuration de leur .env local

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.1 - Bob et Claire mettent à jour leur .env local
────────────────────────────────────────────────────────────────────────────────

BOB ouvre son fichier .env (créé dans le guide SonarQube) et ajoute:

  CONTENU MIS À JOUR DE .env:
  ────────────────────────────
  # ── SonarQube (existant) ──────────────────────────
  SONAR_TOKEN=squ_bob_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
  SONAR_HOST_URL=http://sonar.equipe.local:9000

  # ── ELK Stack (nouveau) ───────────────────────────
  LOGSTASH_HOST=192.168.1.10     # IP de la machine d'Alice
  LOGSTASH_PORT=5000
  KIBANA_URL=http://192.168.1.10:5601
  LOG_LEVEL=DEBUG                # DEBUG en local, INFO en prod
  APP_ENV=development
  LOG_FILE=logs/taskmanager.json

CLAIRE fait exactement pareil avec les mêmes valeurs (même serveur ELK).

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.2 - Bob et Claire vérifient que les logs arrivent dans Kibana
────────────────────────────────────────────────────────────────────────────────

BOB dans son terminal:

  COMMANDES:
  ──────────
  cd taskmanager/
  source venv/bin/activate

  # Charger les variables d'environnement
  export $(grep -v '^#' .env | xargs)

  # Lancer l'application avec les variables ELK
  python run.py

  SORTIE ATTENDUE:
  ────────────────
  [INFO] taskmanager: Logstash handler initialisé logstash_host=192.168.1.10 logstash_port=5000
  [INFO] taskmanager: Application Flask démarrée
   * Running on http://127.0.0.1:5000

  # Dans un autre terminal, générer des requêtes:
  curl http://localhost:5000/tasks
  curl -X POST http://localhost:5000/tasks \
       -H "Content-Type: application/json" \
       -d '{"title": "Tâche de Bob"}'

BOB dans Kibana (http://192.168.1.10:5601):

  1. Se connecter avec: bob / MonNouveauMotDePasse!

  2. Menu ≡ > Analytics > Discover

  3. En haut, sélectionner le data view: "TaskManager Logs"

  4. Définir la plage de temps: "Last 15 minutes"

  5. Bob voit ses logs arriver en temps quasi-réel:
     ┌──────────────────────────────────────────────────────────────────────┐
     │  Discover - TaskManager Logs                          Last 15 min   │
     │  ─────────────────────────────────────────────────────────────────  │
     │  Showing 5 hits                                                      │
     │                                                                      │
     │  Jan 15, 10:30:05.123  INFO  Tâche créée: 'Tâche de Bob'           │
     │    event_type: task_created | task_id: 1 | task_title: Tâche de Bob │
     │                                                                      │
     │  Jan 15, 10:30:05.101  INFO  HTTP POST /tasks -> 201                 │
     │    http_method: POST | http_path: /tasks | http_status: 201         │
     │    duration_ms: 23.4                                                 │
     │                                                                      │
     │  Jan 15, 10:30:00.050  INFO  HTTP GET /tasks -> 200                  │
     │    http_method: GET | http_path: /tasks | http_status: 200          │
     │    duration_ms: 5.2                                                  │
     └──────────────────────────────────────────────────────────────────────┘

  6. Parfait! Les logs de Bob arrivent bien dans la stack ELK partagée.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.3 - Bob et Claire créent leur script de démarrage avec ELK
────────────────────────────────────────────────────────────────────────────────

BOB met à jour son script analyze.sh pour y ajouter un check ELK:

  COMMANDE:
  ─────────
  nano run_dev.sh

  CONTENU:
  ────────
  #!/bin/bash
  # run_dev.sh - Lance l'app Flask avec la stack ELK

  set -e

  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "  TaskManager - Démarrage avec ELK"
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

  # Charger les variables d'environnement
  if [ -f .env ]; then
      export $(grep -v '^#' .env | xargs)
  fi

  # Vérifier qu'Elasticsearch est accessible
  echo "[RECHERCHE] Vérification d'Elasticsearch..."
  ES_URL="http://${LOGSTASH_HOST:-localhost}:9200"
  if curl -s "$ES_URL/_cluster/health" | grep -q '"status"'; then
      echo "[OK] Elasticsearch accessible sur $ES_URL"
  else
      echo "[ATTENTION]  Elasticsearch non accessible sur $ES_URL"
      echo "   Les logs seront écrits uniquement dans logs/taskmanager.json"
  fi

  # Vérifier que Logstash est accessible
  echo "[RECHERCHE] Vérification de Logstash..."
  if nc -z "${LOGSTASH_HOST:-localhost}" "${LOGSTASH_PORT:-5000}" 2>/dev/null; then
      echo "[OK] Logstash accessible sur ${LOGSTASH_HOST}:${LOGSTASH_PORT}"
  else
      echo "[ATTENTION]  Logstash non accessible"
  fi

  echo ""
  echo "[RAPIDE] Démarrage de Flask..."
  echo "[GRAPHIQUE] Voir les logs dans Kibana: ${KIBANA_URL:-http://localhost:5601}"
  echo ""
  python run.py

  COMMANDE:
  ─────────
  chmod +x run_dev.sh

CLAIRE fait la même chose.


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 3 - WORKFLOW QUOTIDIEN DE L'ÉQUIPE (CAS CONCRETS)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Voici 3 scénarios réels de l'équipe utilisant ELK au quotidien.

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO A - Bob détecte et diagnostique une régression de performance
════════════════════════════════════════════════════════════════════════════════

Bob a ajouté une nouvelle fonctionnalité de pagination. Après le déploiement,
des utilisateurs se plaignent que la liste des tâches est lente.

──────────────────────────────────────────────────────────────────────────────
A.1 - Bob cherche des preuves dans Kibana (Discover)
──────────────────────────────────────────────────────────────────────────────

INTERFACE WEB Kibana:

  1. Bob va dans Menu ≡ > Analytics > Discover
  2. Data view: "TaskManager Logs"
  3. Plage de temps: "Last 24 hours"

  4. Bob tape dans la barre KQL (Kibana Query Language):
     http_path: "/tasks" AND http_method: "GET" AND duration_ms > 200

  5. Il voit:
     ┌──────────────────────────────────────────────────────────────────────┐
     │  Discover - Résultats filtrés: 47 hits sur les 24 dernières heures  │
     │                                                                      │
     │  Jan 15, 14:22:11  INFO  HTTP GET /tasks -> 200                      │
     │    duration_ms: 1250.3  <- TRES LENT!                                │
     │    task_count: 5000     <- 5000 tâches retournées!                   │
     │                                                                      │
     │  Jan 15, 14:21:45  INFO  HTTP GET /tasks -> 200                      │
     │    duration_ms: 1180.7                                               │
     │    task_count: 5000                                                  │
     │                                                                      │
     │  Jan 15, 14:20:33  INFO  HTTP GET /tasks -> 200                      │
     │    duration_ms: 234.1                                                │
     │                                                                      │
     │  ...                                                                 │
     └──────────────────────────────────────────────────────────────────────┘

  6. Bob clique sur le premier résultat pour voir tous ses champs:
     ┌──────────────────────────────────────────────────────────────────────┐
     │  Document details                                                    │
     │  ────────────────                                                    │
     │  @timestamp   2024-01-15T14:22:11.123Z                             │
     │  level        INFO                                                   │
     │  message      HTTP GET /tasks -> 200                                 │
     │  event_type   http_request                                          │
     │  http_method  GET                                                    │
     │  http_path    /tasks                                                 │
     │  http_status  200                                                    │
     │  duration_ms  1250.3                                                 │
     │  task_count   5000           <- La base de données a 5000 tâches!   │
     │  slow_request true           <- Tagué comme requête lente            │
     │  tags         ["slow"]                                               │
     └──────────────────────────────────────────────────────────────────────┘

──────────────────────────────────────────────────────────────────────────────
A.2 - Bob crée un graphe de corrélation dans le dashboard
──────────────────────────────────────────────────────────────────────────────

INTERFACE WEB Kibana - Ajouter une visualisation au dashboard existant:

  1. Aller dans Dashboards > "TaskManager - Performances"
  2. Cliquer "Edit" puis "Create visualization"

  Nouvelle visualisation: "Corrélation tâches vs durée"
  - Type: XY Chart (nuage de points / line)
  - X: task_count (Terms, buckets de 100)
  - Y: Average of duration_ms
  - Titre: "Durée selon le nombre de tâches"

  3. Le graphe montre clairement:
     ┌──────────────────────────────────────────────────────────────────┐
     │  Durée (ms)                                                      │
     │  1400 │                                              [BLACK_CIRCLE]           │
     │  1200 │                                         [BLACK_CIRCLE]                │
     │  1000 │                                    [BLACK_CIRCLE]                     │
     │   800 │                               [BLACK_CIRCLE]                          │
     │   600 │                          [BLACK_CIRCLE]                               │
     │   400 │                    [BLACK_CIRCLE][BLACK_CIRCLE]                                    │
     │   200 │              [BLACK_CIRCLE][BLACK_CIRCLE][BLACK_CIRCLE]                                         │
     │     0 │──────────────────────────────────────── task_count       │
     │       0    1000   2000   3000   4000   5000                      │
     └──────────────────────────────────────────────────────────────────┘

  La durée augmente linéairement avec le nombre de tâches.
  Pas de pagination -> tout est chargé en mémoire!

──────────────────────────────────────────────────────────────────────────────
A.3 - Bob corrige le problème et loggue la correction
──────────────────────────────────────────────────────────────────────────────

Bob corrige app/routes.py pour ajouter la pagination:

  ── CODE CORRIGÉ ─────────────────────────────────────────────────────────────

  @tasks_bp.route('/tasks', methods=['GET'])
  def get_tasks():
      # [OK] Pagination ajoutée
      page     = request.args.get('page', 1, type=int)
      per_page = min(request.args.get('per_page', 20, type=int), 100)

      tasks_paginated = Task.query.paginate(
          page=page, per_page=per_page, error_out=False
      )

      logger.info(
          f"Liste paginée: page {page}, {len(tasks_paginated.items)} tâches",
          extra={
              "event_type":   "task_list",
              "task_count":   len(tasks_paginated.items),
              "page":         page,
              "per_page":     per_page,
              "total":        tasks_paginated.total,
              "has_next":     tasks_paginated.has_next,
          }
      )
      return jsonify({
          "tasks":    [t.to_dict() for t in tasks_paginated.items],
          "total":    tasks_paginated.total,
          "page":     page,
          "per_page": per_page,
          "pages":    tasks_paginated.pages
      })

──────────────────────────────────────────────────────────────────────────────
A.4 - Bob vérifie l'amélioration dans Kibana
──────────────────────────────────────────────────────────────────────────────

Après déploiement, Bob retourne dans Kibana > Discover:

  Filtre KQL: event_type: "task_list" AND @timestamp > "now-1h"

  Nouveaux logs:
  ┌──────────────────────────────────────────────────────────────────────┐
  │  Jan 15, 15:00:11  INFO  Liste paginée: page 1, 20 tâches           │
  │    task_count: 20 | page: 1 | total: 5000 | duration_ms: 12.3      │
  │                                                                      │
  │  Jan 15, 15:00:08  INFO  Liste paginée: page 1, 20 tâches           │
  │    task_count: 20 | page: 1 | total: 5000 | duration_ms: 11.7      │
  └──────────────────────────────────────────────────────────────────────┘

  La durée est passée de ~1250ms à ~12ms: 100x plus rapide!


════════════════════════════════════════════════════════════════════════════════
SCÉNARIO B - Claire analyse les erreurs 404 et identifie des appels aberrants
════════════════════════════════════════════════════════════════════════════════

Claire remarque dans le dashboard que le nombre d'erreurs 404 est anormalement
élevé. Elle mène l'enquête dans Kibana.

──────────────────────────────────────────────────────────────────────────────
B.1 - Claire utilise la vue Discover avec des filtres avancés
──────────────────────────────────────────────────────────────────────────────

INTERFACE WEB Kibana:

  1. Menu ≡ > Analytics > Discover
  2. Plage de temps: "Last 7 days"
  3. Filtre KQL: http_status: 404

  4. Claire voit beaucoup de 404 sur des chemins suspects:
     ┌──────────────────────────────────────────────────────────────────────┐
     │  Résultats: 1,247 hits                                               │
     │                                                                      │
     │  Jan 15, 14:00:01  WARNING  Ressource non trouvée: /tasks/99999     │
     │  Jan 15, 14:00:01  WARNING  Ressource non trouvée: /tasks/99998     │
     │  Jan 15, 14:00:01  WARNING  Ressource non trouvée: /tasks/99997     │
     │  Jan 15, 14:00:01  WARNING  Ressource non trouvée: /tasks/99996     │
     │  ... (1000 requêtes en 2 secondes!)                                  │
     └──────────────────────────────────────────────────────────────────────┘

  5. Claire voit que quelqu'un énumère les IDs de manière séquentielle
     (attaque par énumération / brute force).

──────────────────────────────────────────────────────────────────────────────
B.2 - Claire crée une visualisation "Top IPs suspectes"
──────────────────────────────────────────────────────────────────────────────

INTERFACE WEB Kibana:

  1. Aller dans Dashboards > "TaskManager - Erreurs"
  2. Ajouter une nouvelle visualisation

  - Type: Data table
  - Filtre: http_status: 404
  - Colonnes: remote_addr (Terms, top 10), Count
  - Titre: "Top IPs générant des 404"

  Résultat:
  ┌──────────────────────────────────────────────────────────────────────┐
  │  Top IPs générant des 404                                            │
  ├───────────────────┬───────────────────────────────────────────────┤
  │  IP               │  Nombre de 404                                 │
  ├───────────────────┼───────────────────────────────────────────────┤
  │  192.168.1.99     │  1,032  <- SUSPECT!                            │
  │  192.168.1.50     │  12                                            │
  │  127.0.0.1        │  5                                             │
  └───────────────────┴───────────────────────────────────────────────┘

──────────────────────────────────────────────────────────────────────────────
B.3 - Claire ajoute un log de sécurité dans le code et prévient Alice
──────────────────────────────────────────────────────────────────────────────

Claire ajoute dans app/routes.py une détection basique de comportement suspect:

  ── CODE AJOUTÉ PAR CLAIRE ───────────────────────────────────────────────────

  from collections import defaultdict
  from datetime import datetime

  # Compteur simple en mémoire (en prod, utiliser Redis)
  _404_counter = defaultdict(list)

  def check_suspicious_activity(ip, path):
      """Log un avertissement si une IP génère trop de 404."""
      now = datetime.utcnow()
      # Garder les 60 dernières secondes
      _404_counter[ip] = [t for t in _404_counter[ip]
                           if (now - t).seconds < 60]
      _404_counter[ip].append(now)

      count = len(_404_counter[ip])
      if count >= 10:
          logger.warning(
              f"Activité suspecte détectée: {count} erreurs 404 en 60s",
              extra={
                  "event_type":       "suspicious_activity",
                  "remote_addr":      ip,
                  "error_count":      count,
                  "last_path":        path,
                  "security_alert":   True,
              }
          )

  # Dans chaque route qui génère un 404:
  @tasks_bp.route('/tasks/<int:task_id>', methods=['GET'])
  def get_task(task_id):
      task = Task.query.get(task_id)
      if task is None:
          check_suspicious_activity(request.remote_addr, request.path)
          logger.warning(
              f"Tâche {task_id} introuvable",
              extra={"event_type": "task_not_found", "task_id": task_id}
          )
          abort(404, description="Task not found")
      ...

INTERFACE WEB Kibana - Claire configure une alerte:

  1. Menu ≡ > Management > Stack Management > Alerts and Insights > Rules

  2. Cliquer "+ Create rule"

  3. Sélectionner "Elasticsearch query" comme type de règle:
     ┌────────────────────────────────────────────────────────────────┐
     │  Create rule                                                   │
     │                                                                │
     │  Name: Activité suspecte - Trop de 404                       │
     │                                                                │
     │  Index: taskmanager-logs-*                                    │
     │  Time field: @timestamp                                       │
     │                                                                │
     │  Query (KQL):                                                 │
     │  security_alert: true AND event_type: "suspicious_activity"  │
     │                                                                │
     │  Threshold: > 0 hits in last 5 minutes                       │
     │                                                                │
     │  Actions:                                                      │
     │  Email to: alice@equipe.com, claire@equipe.com               │
     │  Subject: [ALERTE] Activité suspecte TaskManager              │
     │                                                                │
     │  Check interval: Every 5 minutes                              │
     │                                                                │
     │  [   Save rule   ]                                             │
     └────────────────────────────────────────────────────────────────┘

  4. Alice et Claire recevront maintenant un email automatique si
     une activité suspecte est détectée.


════════════════════════════════════════════════════════════════════════════════
SCÉNARIO C - Alice fait la revue hebdomadaire des métriques avec l'équipe
════════════════════════════════════════════════════════════════════════════════

Chaque lundi matin, Alice présente les métriques de la semaine via Kibana.

──────────────────────────────────────────────────────────────────────────────
C.1 - Alice prépare le rapport hebdomadaire avec les agrégations Kibana
──────────────────────────────────────────────────────────────────────────────

INTERFACE WEB Kibana:

  1. Alice va dans Dashboards > "TaskManager - Vue Générale"
  2. Elle change la plage de temps: "Last 7 days"
  3. Elle constate:

  ┌───────────────────────────────────────────────────────────────────────┐
  │  TaskManager - Vue Générale │ 7 derniers jours │                     │
  │                                                                       │
  │  Requêtes totales: 15,247                                            │
  │  Durée moyenne:    45ms [OK]                                            │
  │  Taux d'erreur:    2.3% [OK]                                            │
  │                                                                       │
  │  Répartition codes HTTP:                                             │
  │  [VERT] 200: 11,832 (77.6%)                                              │
  │  [VERT] 201:  2,150 (14.1%)                                              │
  │  [JAUNE] 400:    412 (2.7%)                                               │
  │  [ROUGE] 404:    802 (5.3%)   <- En hausse!                               │
  │  [ROUGE] 500:     51 (0.3%)                                               │
  │                                                                       │
  │  Top routes:                                                         │
  │  1. GET /tasks        -> 8,232 appels, 38ms moy                      │
  │  2. POST /tasks       -> 3,411 appels, 52ms moy                      │
  │  3. GET /tasks/{id}   -> 2,891 appels, 29ms moy                      │
  │  4. GET /tasks/search -> 1,203 appels, 87ms moy <- la + lente        │
  └───────────────────────────────────────────────────────────────────────┘

──────────────────────────────────────────────────────────────────────────────
C.2 - Alice utilise Lens pour créer une analyse ad-hoc
──────────────────────────────────────────────────────────────────────────────

Alice veut comprendre l'évolution quotidienne des erreurs 500 cette semaine.

INTERFACE WEB Kibana - Lens (outil d'analyse visuelle):

  1. Menu ≡ > Analytics > Visualize Library
  2. Cliquer "+ Create visualization"
  3. Sélectionner "Lens" (outil de glisser-déposer)

  4. Interface Lens:
     ┌─────────────────────────────────────────────────────────────────────┐
     │  Lens Editor                                                        │
     │  ─────────────                                                      │
     │  [Champs disponibles]    │   [Zone de construction]                 │
     │                          │                                          │
     │  [TRIGRAM_FOR_HEAVEN] @timestamp            │   Type: Bar stacked                      │
     │  [TRIGRAM_FOR_HEAVEN] level                 │                                          │
     │  [TRIGRAM_FOR_HEAVEN] http_status           │   Horizontal axis:                       │
     │  [TRIGRAM_FOR_HEAVEN] duration_ms           │   [Glisser @timestamp ici]              │
     │  [TRIGRAM_FOR_HEAVEN] event_type            │   -> Intervalle: 1 jour                  │
     │  [TRIGRAM_FOR_HEAVEN] remote_addr           │                                          │
     │  [TRIGRAM_FOR_HEAVEN] task_id               │   Vertical axis:                         │
     │  [TRIGRAM_FOR_HEAVEN] ...                   │   [Glisser http_status ici]             │
     │                          │   -> Count (avec filtre: >= 500)          │
     └─────────────────────────────────────────────────────────────────────┘

  5. Alice glisse @timestamp sur l'axe X et configure:
     - Granularité: 1 jour
  6. Elle glisse http_status sur l'axe Y et configure:
     - Agrégation: Count
     - Filtre: http_status >= 500
  7. Le graphe s'affiche instantanément:

     Erreurs 500 par jour:
     Lun: 3
     Mar: 7  <- Pic, corrélé avec le déploiement de Bob?
     Mer: 5
     Jeu: 2
     Ven: 18 <- PROBLÈME! Claire a poussé quelque chose vendredi?
     Sam: 0
     Dim: 1

  8. Alice sauvegarde cette visualisation et l'ajoute au dashboard Erreurs.

──────────────────────────────────────────────────────────────────────────────
C.3 - Alice utilise la recherche temporelle pour investiguer le vendredi
──────────────────────────────────────────────────────────────────────────────

INTERFACE WEB Kibana - Investigate le pic du vendredi:

  1. Menu ≡ > Analytics > Discover
  2. Plage de temps: Vendredi 09:00 -> Vendredi 23:59
  3. Filtre KQL: http_status: 500

  4. Alice voit:
     ┌──────────────────────────────────────────────────────────────────────┐
     │  18 erreurs 500 le vendredi                                          │
     │                                                                      │
     │  Jan 12, 17:42:00  ERROR  Erreur HTTP POST /tasks -> 500             │
     │    error: "UNIQUE constraint failed: task.title"                     │
     │    traceback: sqlalchemy.exc.IntegrityError...                      │
     │                                                                      │
     │  Jan 12, 17:41:58  ERROR  Erreur HTTP POST /tasks -> 500             │
     │    (même erreur, 18 fois entre 17h41 et 17h52)                      │
     └──────────────────────────────────────────────────────────────────────┘

  5. Alice voit que quelqu'un a ajouté une contrainte UNIQUE sur le titre
     des tâches mais n'a pas géré l'erreur dans l'API.
     -> C'est le commit de Claire de vendredi après-midi.

  6. Alice crée une issue dans le tracker et l'assigne à Claire.

──────────────────────────────────────────────────────────────────────────────
C.4 - Alice partage un lien vers la vue sauvegardée avec Bob et Claire
──────────────────────────────────────────────────────────────────────────────

INTERFACE WEB Kibana - Partager une vue:

  1. Dans Discover (avec les filtres actifs), cliquer sur l'icône "Share" (->)

  2. Kibana affiche:
     ┌────────────────────────────────────────────────────────────────┐
     │  Share                                                         │
     │                                                                │
     │  [BLACK_CIRCLE] Snapshot (URL courte, état actuel)                         │
     │  [WHITE_CIRCLE] Saved object (lien vers la sauvegarde)                     │
     │                                                                │
     │  URL courte:                                                   │
     │  http://192.168.1.10:5601/goto/abc123def456                   │
     │                                                                │
     │  [  Copy link  ]                                               │
     └────────────────────────────────────────────────────────────────┘

  3. Alice copie le lien et l'envoie à Claire par message:
     "Claire, vois les erreurs 500 de vendredi: http://192.168.1.10:5601/goto/abc123def456
      Les 18 erreurs viennent de la contrainte UNIQUE non gérée. Peux-tu corriger?"


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 4 - REQUÊTES KQL ET DSL UTILES POUR L'ÉQUIPE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Ces requêtes sont utilisées au quotidien dans Kibana Discover.

────────────────────────────────────────────────────────────────────────────────
REQUÊTES KQL (Kibana Query Language) - Simples et intuitives
────────────────────────────────────────────────────────────────────────────────

  # Toutes les erreurs:
  level: "ERROR"

  # Toutes les erreurs HTTP:
  http_status >= 400

  # Erreurs serveur seulement:
  http_status >= 500

  # Requêtes lentes (>500ms):
  duration_ms > 500

  # Une route spécifique:
  http_path: "/tasks/search"

  # Tâches créées:
  event_type: "task_created"

  # Tâches complétées:
  event_type: "task_completed"

  # Alertes sécurité:
  security_alert: true

  # Combinaisons avec AND/OR:
  http_status: 404 AND remote_addr: "192.168.1.99"
  level: "ERROR" OR level: "WARNING"
  event_type: "task_created" AND NOT level: "ERROR"

  # Recherche dans le message:
  message: "contrainte" OR message: "UNIQUE"

  # Plage de valeurs:
  duration_ms >= 100 AND duration_ms <= 500

  # Existence d'un champ:
  task_id: *          <- Tous les logs avec task_id
  NOT task_id: *      <- Tous les logs sans task_id

────────────────────────────────────────────────────────────────────────────────
REQUÊTES API ELASTICSEARCH DIRECTES (via curl ou Kibana Dev Tools)
────────────────────────────────────────────────────────────────────────────────

Menu Kibana: ≡ > Management > Dev Tools (console d'exécution de requêtes)

  ── Requête 1: Compter les requêtes par statut HTTP ──────────────────────────

  GET /taskmanager-logs-*/_search
  {
    "size": 0,
    "aggs": {
      "par_statut": {
        "terms": {
          "field": "http_status",
          "size": 10
        }
      }
    }
  }

  Résultat:
  {
    "aggregations": {
      "par_statut": {
        "buckets": [
          { "key": 200, "doc_count": 11832 },
          { "key": 201, "doc_count": 2150 },
          { "key": 404, "doc_count": 802 },
          { "key": 400, "doc_count": 412 },
          { "key": 500, "doc_count": 51 }
        ]
      }
    }
  }

  ── Requête 2: Durée moyenne et percentiles par route ────────────────────────

  GET /taskmanager-logs-*/_search
  {
    "size": 0,
    "query": {
      "exists": { "field": "http_path" }
    },
    "aggs": {
      "par_route": {
        "terms": {
          "field": "http_path.keyword",
          "size": 10
        },
        "aggs": {
          "duree_moyenne": { "avg": { "field": "duration_ms" } },
          "percentiles": {
            "percentiles": {
              "field": "duration_ms",
              "percents": [50, 90, 99]
            }
          }
        }
      }
    }
  }

  ── Requête 3: Top IPs avec le plus de 404 des 24 dernières heures ──────────

  GET /taskmanager-logs-*/_search
  {
    "size": 0,
    "query": {
      "bool": {
        "filter": [
          { "term":  { "http_status": 404 } },
          { "range": { "@timestamp": { "gte": "now-24h" } } }
        ]
      }
    },
    "aggs": {
      "top_ips": {
        "terms": {
          "field": "remote_addr.keyword",
          "size": 5
        }
      }
    }
  }

  ── Requête 4: Erreurs groupées par heure pour les dernières 48h ─────────────

  GET /taskmanager-logs-*/_search
  {
    "size": 0,
    "query": {
      "bool": {
        "filter": [
          { "range":  { "@timestamp": { "gte": "now-48h" } } },
          { "range":  { "http_status": { "gte": 500 } } }
        ]
      }
    },
    "aggs": {
      "erreurs_par_heure": {
        "date_histogram": {
          "field": "@timestamp",
          "calendar_interval": "1h"
        }
      }
    }
  }

  ── Requête 5: Vérifier que les logs arrivent (santé de l'indexation) ─────────

  GET /taskmanager-logs-*/_count

  GET /_cat/indices/taskmanager-logs-*?v&s=index

  GET /_cat/shards/taskmanager-logs-*?v

────────────────────────────────────────────────────────────────────────────────
SCRIPT PYTHON POUR REQUÊTES ELASTICSEARCH DEPUIS L'APPLICATION
────────────────────────────────────────────────────────────────────────────────

Parfois l'équipe veut interroger Elasticsearch directement depuis Python:

  INSTALLATION:
  ─────────────
  pip install elasticsearch

  SCRIPT app/elk_queries.py:
  ──────────────────────────

  """
  Requêtes utilitaires Elasticsearch pour l'application TaskManager.
  Utile pour des rapports internes ou des vérifications de santé.
  """
  from elasticsearch import Elasticsearch
  import os

  ES_HOST = os.environ.get('ELASTICSEARCH_HOST', 'localhost')
  ES_PORT = int(os.environ.get('ELASTICSEARCH_PORT', 9200))

  def get_es_client():
      """Retourne un client Elasticsearch connecté."""
      return Elasticsearch([f"http://{ES_HOST}:{ES_PORT}"])

  def get_daily_stats(date_str=None):
      """
      Retourne les statistiques du jour: nb requêtes, taux d'erreur, durée moy.

      Args:
          date_str: Date au format "YYYY.MM.DD" (par défaut: aujourd'hui)

      Returns:
          dict avec les métriques du jour
      """
      from datetime import datetime
      if not date_str:
          date_str = datetime.utcnow().strftime('%Y.%m.%d')

      es = get_es_client()
      index = f"taskmanager-logs-{date_str}"

      try:
          result = es.search(
              index=index,
              body={
                  "size": 0,
                  "aggs": {
                      "total":          { "value_count": { "field": "@timestamp" } },
                      "errors_500":     { "filter": { "range": { "http_status": { "gte": 500 } } } },
                      "duree_moyenne":  { "avg":   { "field": "duration_ms" } },
                      "duree_p99":      { "percentiles": { "field": "duration_ms", "percents": [99] } }
                  }
              }
          )

          aggs = result['aggregations']
          total = aggs['total']['value']

          return {
              "date":             date_str,
              "total_requests":   total,
              "error_500_count":  aggs['errors_500']['doc_count'],
              "error_rate_pct":   round(aggs['errors_500']['doc_count'] / max(total, 1) * 100, 2),
              "avg_duration_ms":  round(aggs['duree_moyenne']['value'] or 0, 2),
              "p99_duration_ms":  round(aggs['duree_p99']['values']['99.0'] or 0, 2),
          }

      except Exception as e:
          return {"error": str(e), "date": date_str}


  def get_slow_requests(threshold_ms=500, hours=24):
      """Retourne les requêtes les plus lentes des N dernières heures."""
      es = get_es_client()

      result = es.search(
          index="taskmanager-logs-*",
          body={
              "size": 10,
              "query": {
                  "bool": {
                      "filter": [
                          { "range": { "duration_ms":  { "gte": threshold_ms } } },
                          { "range": { "@timestamp":   { "gte": f"now-{hours}h" } } }
                      ]
                  }
              },
              "sort": [{ "duration_ms": "desc" }],
              "_source": ["@timestamp", "http_method", "http_path", "duration_ms", "http_status"]
          }
      )

      return [hit['_source'] for hit in result['hits']['hits']]


  if __name__ == "__main__":
      print("[GRAPHIQUE] Statistiques du jour:")
      stats = get_daily_stats()
      for key, value in stats.items():
          print(f"   {key}: {value}")

      print("\n[TURTLE] 10 requêtes les plus lentes (24h):")
      slow = get_slow_requests()
      for req in slow:
          print(f"   {req.get('http_method')} {req.get('http_path')} -> {req.get('duration_ms')}ms")


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 5 - TESTS UNITAIRES AVEC ELK (NE PAS ENVOYER DE LOGS EN TEST)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Claire s'occupe de s'assurer que les tests ne polluent pas Elasticsearch
avec de faux logs.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 5.1 - Configurer les tests pour désactiver les logs ELK
────────────────────────────────────────────────────────────────────────────────

Claire met à jour tests/conftest.py:

  CONTENU MIS À JOUR DE tests/conftest.py:
  ──────────────────────────────────────────

  import pytest
  import logging
  from app import create_app, db as _db

  @pytest.fixture(autouse=True)
  def disable_logstash_in_tests(monkeypatch):
      """
      Désactive automatiquement l'envoi vers Logstash pendant les tests.
      - Définit LOGSTASH_HOST à une adresse invalide
      - Supprime les handlers Logstash du logger
      Cette fixture est appliquée automatiquement à TOUS les tests (autouse=True).
      """
      monkeypatch.setenv('LOGSTASH_HOST', '127.0.0.1')
      monkeypatch.setenv('LOGSTASH_PORT', '1')       # Port invalide
      monkeypatch.setenv('APP_ENV', 'testing')
      monkeypatch.setenv('LOG_FILE', '/dev/null')    # Logs dans le vide

      # Supprimer tous les handlers Logstash existants
      import logstash
      root_logger = logging.getLogger('taskmanager')
      handlers_to_remove = [
          h for h in root_logger.handlers
          if isinstance(h, logstash.TCPLogstashHandler)
      ]
      for h in handlers_to_remove:
          root_logger.removeHandler(h)

      yield

      # Remettre les handlers après le test (optionnel car monkeypatch les restaure)


  @pytest.fixture
  def app():
      test_config = {
          'TESTING': True,
          'SQLALCHEMY_DATABASE_URI': 'sqlite:///:memory:'
      }
      app = create_app(test_config)
      with app.app_context():
          _db.create_all()
          yield app
          _db.drop_all()


  @pytest.fixture
  def client(app):
      return app.test_client()

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 5.2 - Tester que le logging fonctionne (sans Logstash)
────────────────────────────────────────────────────────────────────────────────

Claire ajoute des tests pour vérifier que les logs sont bien générés:

  FICHIER tests/test_logging.py:
  ───────────────────────────────

  import logging
  import json
  import pytest


  def test_create_task_logs_event(client, caplog):
      """Vérifier qu'une création de tâche génère bien un log INFO."""
      with caplog.at_level(logging.INFO, logger='taskmanager.routes'):
          response = client.post('/tasks', json={'title': 'Test log'})

      assert response.status_code == 201

      # Vérifier qu'il y a bien un log avec event_type task_created
      log_messages = [record.message for record in caplog.records]
      assert any("Tâche créée" in msg for msg in log_messages), \
          f"Log 'Tâche créée' attendu mais non trouvé. Logs: {log_messages}"


  def test_not_found_logs_warning(client, caplog):
      """Vérifier qu'un 404 génère un log WARNING."""
      with caplog.at_level(logging.WARNING, logger='taskmanager.routes'):
          response = client.get('/tasks/9999')

      assert response.status_code == 404

      warning_logs = [r for r in caplog.records if r.levelno == logging.WARNING]
      assert len(warning_logs) >= 1, "Un WARNING attendu pour le 404"
      assert any("introuvable" in r.message for r in warning_logs)


  def test_invalid_create_logs_warning(client, caplog):
      """Vérifier qu'une création invalide génère un log WARNING."""
      with caplog.at_level(logging.WARNING, logger='taskmanager.routes'):
          response = client.post('/tasks', json={'description': 'sans titre'})

      assert response.status_code == 400

      warning_logs = [r for r in caplog.records if r.levelno == logging.WARNING]
      assert len(warning_logs) >= 1


  def test_http_middleware_logs_request(client, caplog):
      """Vérifier que le middleware logue toutes les requêtes HTTP."""
      with caplog.at_level(logging.INFO, logger='taskmanager'):
          client.get('/tasks')

      # Chercher un log avec HTTP GET /tasks
      http_logs = [r for r in caplog.records if 'HTTP GET /tasks' in r.message]
      assert len(http_logs) >= 1


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 6 - INTÉGRATION CI/CD - AJOUT ELK DANS GITHUB ACTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Alice met à jour le fichier GitHub Actions pour intégrer les vérifications ELK.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 6.1 - Mise à jour du workflow GitHub Actions
────────────────────────────────────────────────────────────────────────────────

  COMMANDE:
  ─────────
  nano .github/workflows/sonarqube.yml

  CONTENU MIS À JOUR (ajout des étapes ELK):
  ───────────────────────────────────────────

  name: CI - SonarQube + Tests + ELK check

  on:
    push:
      branches: [main, develop, 'feature/**']
    pull_request:
      branches: [main]

  jobs:
    tests-and-analysis:
      name: Tests, SonarQube et vérification ELK
      runs-on: ubuntu-latest

      # ── Services Docker: Elasticsearch léger pour les tests ──────────────
      services:
        elasticsearch:
          image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
          env:
            discovery.type: single-node
            xpack.security.enabled: "false"
            ES_JAVA_OPTS: "-Xms256m -Xmx256m"
          ports:
            - 9200:9200
          options: >-
            --health-cmd "curl -s http://localhost:9200/_cluster/health"
            --health-interval 10s
            --health-timeout 5s
            --health-retries 10

      steps:
        - name: Checkout code
          uses: actions/checkout@v4
          with:
            fetch-depth: 0

        - name: Setup Python 3.11
          uses: actions/setup-python@v4
          with:
            python-version: '3.11'

        - name: Cache pip packages
          uses: actions/cache@v3
          with:
            path: ~/.cache/pip
            key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}

        - name: Install dependencies
          run: |
            python -m pip install --upgrade pip
            pip install -r requirements.txt
            pip install -r requirements-dev.txt

        # ── Étape ELK: Vérifier qu'Elasticsearch est accessible ──────────
        - name: Wait for Elasticsearch
          run: |
            echo "Attente d'Elasticsearch..."
            for i in $(seq 1 30); do
              if curl -s http://localhost:9200/_cluster/health | grep -q '"status"'; then
                echo "[OK] Elasticsearch prêt!"
                break
              fi
              echo "Tentative $i/30..."
              sleep 5
            done

        # ── Étape ELK: Vérifier la santé du cluster ───────────────────────
        - name: Check Elasticsearch health
          run: |
            HEALTH=$(curl -s http://localhost:9200/_cluster/health)
            STATUS=$(echo $HEALTH | python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
            echo "Elasticsearch status: $STATUS"
            if [ "$STATUS" != "green" ] && [ "$STATUS" != "yellow" ]; then
              echo "[X] Elasticsearch unhealthy: $STATUS"
              exit 1
            fi
            echo "[OK] Elasticsearch healthy: $STATUS"

        - name: Run tests with coverage
          run: |
            pytest \
              --cov=app \
              --cov-report=xml \
              --cov-report=term-missing \
              -v
          env:
            TESTING: "true"
            LOGSTASH_HOST: "127.0.0.1"
            LOGSTASH_PORT: "1"      # Désactivé en CI (utilise Filebeat en prod)
            APP_ENV: "testing"
            LOG_FILE: "/dev/null"
            ELASTICSEARCH_HOST: "localhost"
            ELASTICSEARCH_PORT: "9200"

        # ── Étape ELK: Vérifier l'indexation d'un log test ───────────────
        - name: Test Elasticsearch indexing
          run: |
            echo "Test d'indexation dans Elasticsearch..."
            # Indexer un document test
            curl -s -X POST "http://localhost:9200/test-index/_doc/" \
              -H "Content-Type: application/json" \
              -d '{"message": "CI test", "level": "INFO", "app": "taskmanager-api"}' \
              | python3 -c "import sys,json; r=json.load(sys.stdin); print('[OK] Indexé:', r['result'])"

            # Vérifier la récupération
            sleep 1
            COUNT=$(curl -s "http://localhost:9200/test-index/_count" \
              | python3 -c "import sys,json; print(json.load(sys.stdin)['count'])")
            echo "Documents dans l'index test: $COUNT"
            if [ "$COUNT" -lt "1" ]; then
              echo "[X] L'indexation a échoué"
              exit 1
            fi
            echo "[OK] Elasticsearch indexation OK"

        - name: Cache SonarQube scanner
          uses: actions/cache@v3
          with:
            path: ~/.sonar/cache
            key: ${{ runner.os }}-sonar

        - name: SonarQube Scan
          uses: sonarsource/sonarqube-scan-action@master
          env:
            SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
            SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

        - name: Check SonarQube Quality Gate
          uses: sonarsource/sonarqube-quality-gate-action@master
          timeout-minutes: 5
          env:
            SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 7 - MAINTENANCE ET GESTION DES INDEX ELASTICSEARCH
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Alice configure la rotation et l'archivage automatique des index.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 7.1 - Configurer une Index Lifecycle Policy (ILM)
────────────────────────────────────────────────────────────────────────────────

INTERFACE WEB Kibana - Stack Management > Index Lifecycle Policies:

  1. Menu ≡ > Management > Stack Management > Data > Index Lifecycle Policies
  2. Cliquer "+ Create policy"
  3. Configurer la politique "taskmanager-logs-policy":

  ┌────────────────────────────────────────────────────────────────────┐
  │  Create Index Lifecycle Policy                                     │
  │  Name: taskmanager-logs-policy                                     │
  │                                                                    │
  │  HOT PHASE (index actif, écritures rapides)                       │
  │  ─────────────────────────────────────────                        │
  │  [OK] Activée                                                       │
  │  Roll over:                                                        │
  │    [WHITE_CIRCLE] Maximum age: 1 day     <- Tourner chaque jour                 │
  │    [WHITE_CIRCLE] Maximum size: 10 GB    <- Ou si l'index dépasse 10GB          │
  │                                                                    │
  │  WARM PHASE (index ancien, lecture seule)                         │
  │  ──────────────────────────────────────                           │
  │  [OK] Activée - après 7 jours                                       │
  │  - Passer en read-only                                            │
  │  - Forcer la fusion (réduire les segments)                        │
  │                                                                    │
  │  COLD PHASE (archivage longue durée)                              │
  │  ─────────────────────────────────                                │
  │  [OK] Activée - après 30 jours                                      │
  │  - Déplacer vers un nœud "cold" (moins coûteux)                  │
  │                                                                    │
  │  DELETE PHASE (suppression automatique)                           │
  │  ──────────────────────────────────────                           │
  │  [OK] Activée - après 90 jours                                      │
  │  - Supprimer l'index complètement                                 │
  │                                                                    │
  │  [   Save policy   ]                                               │
  └────────────────────────────────────────────────────────────────────┘

  RÉSULTAT: Les logs seront:
  - Conservés 90 jours au total
  - Optimisés automatiquement après 7 jours
  - Supprimés automatiquement après 90 jours

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 7.2 - Configurer un Index Template
────────────────────────────────────────────────────────────────────────────────

Alice crée un template pour que chaque nouvel index soit créé avec les bons
paramètres (mappings, politique ILM, etc.):

VIA KIBANA DEV TOOLS:

  PUT /_index_template/taskmanager-logs-template
  {
    "index_patterns": ["taskmanager-logs-*"],
    "template": {
      "settings": {
        "number_of_shards":   1,
        "number_of_replicas": 0,
        "lifecycle.name":     "taskmanager-logs-policy",
        "refresh_interval":   "5s"
      },
      "mappings": {
        "properties": {
          "@timestamp":    { "type": "date" },
          "level":         { "type": "keyword" },
          "message":       { "type": "text" },
          "event_type":    { "type": "keyword" },
          "http_method":   { "type": "keyword" },
          "http_path":     { "type": "keyword" },
          "http_status":   { "type": "integer" },
          "duration_ms":   { "type": "float" },
          "task_id":       { "type": "integer" },
          "task_title":    { "type": "text" },
          "remote_addr":   { "type": "ip" },
          "slow_request":  { "type": "boolean" },
          "security_alert":{ "type": "boolean" },
          "app_name":      { "type": "keyword" },
          "environment":   { "type": "keyword" },
          "module":        { "type": "keyword" },
          "func_name":     { "type": "keyword" }
        }
      }
    }
  }

  RÉSULTAT ATTENDU:
  {
    "acknowledged": true
  }

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 7.3 - Commandes de maintenance courantes
────────────────────────────────────────────────────────────────────────────────

  COMMANDES CURL utiles (à ajouter dans un script maintenance.sh):
  ─────────────────────────────────────────────────────────────────

  # Lister tous les index taskmanager avec leur taille:
  curl -s "http://localhost:9200/_cat/indices/taskmanager-logs-*?v&s=index&h=index,docs.count,store.size,creation.date.string"

  # Voir le statut de la politique ILM sur un index:
  curl -s "http://localhost:9200/taskmanager-logs-*/_ilm/explain?pretty"

  # Supprimer manuellement un index ancien:
  curl -X DELETE "http://localhost:9200/taskmanager-logs-2024.01.01"

  # Compter les documents dans tous les index:
  curl -s "http://localhost:9200/taskmanager-logs-*/_count?pretty"

  # Vérifier les erreurs dans Logstash:
  docker logs logstash --tail 50

  # Redémarrer Logstash si pipeline bloqué:
  docker restart logstash

  # Vérifier l'espace disque d'Elasticsearch:
  curl -s "http://localhost:9200/_cat/allocation?v"

  # Forcer le flush d'un index (écrire sur disque):
  curl -X POST "http://localhost:9200/taskmanager-logs-*/_flush"


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 8 - BONNES PRATIQUES DE L'ÉQUIPE (RÈGLES D'OR ELK)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

────────────────────────────────────────────────────────────────────────────────
RÈGLE 1 - Structure des logs: toujours inclure event_type
────────────────────────────────────────────────────────────────────────────────

Chaque log métier DOIT avoir un champ "event_type" pour pouvoir filtrer:

  # [OK] BON LOG: structuré, filtrable, avec contexte
  logger.info(
      "Tâche créée",
      extra={
          "event_type": "task_created",   <- OBLIGATOIRE
          "task_id":    42,
          "task_title": "Acheter du lait"
      }
  )

  # [X] MAUVAIS LOG: non structuré, impossible à filtrer dans Kibana
  logger.info("Task 42 'Acheter du lait' created")

────────────────────────────────────────────────────────────────────────────────
RÈGLE 2 - Niveaux de log: utiliser le bon niveau
────────────────────────────────────────────────────────────────────────────────

  DEBUG    -> Informations de débogage (désactivé en prod)
             Exemple: logger.debug("Requête SQL générée", extra={"sql": query})

  INFO     -> Événements normaux de l'application
             Exemple: logger.info("Tâche créée", extra={"task_id": 42})

  WARNING  -> Situation anormale mais non bloquante
             Exemple: logger.warning("Ressource non trouvée", extra={"task_id": 999})

  ERROR    -> Erreur qui a empêché une opération de se terminer
             Exemple: logger.error("Erreur DB", exc_info=True, extra={"query": q})

  CRITICAL -> Erreur qui menace la stabilité de l'application
             Exemple: logger.critical("Connexion DB perdue!", extra={"host": db_host})

────────────────────────────────────────────────────────────────────────────────
RÈGLE 3 - Ne jamais logger de données sensibles
────────────────────────────────────────────────────────────────────────────────

  # [X] NE JAMAIS FAIRE:
  logger.info("Connexion", extra={"password": user_password})
  logger.info("Token", extra={"token": auth_token})
  logger.info("Carte", extra={"card_number": "4111111111111111"})

  # [OK] FAIRE À LA PLACE:
  logger.info("Connexion utilisateur", extra={"user_id": user.id})
  logger.info("Token généré", extra={"token_prefix": token[:8] + "..."})
  logger.info("Paiement", extra={"card_last4": "1111", "amount": 29.99})

────────────────────────────────────────────────────────────────────────────────
RÈGLE 4 - Un dashboard par "persona" d'observabilité
────────────────────────────────────────────────────────────────────────────────

  "TaskManager - Vue Générale"    -> Pour la réunion du lundi (Alice)
  "TaskManager - Erreurs"         -> Pour investiguer les bugs (Bob, Claire)
  "TaskManager - Performances"    -> Pour optimiser le code (Bob)
  "TaskManager - Sécurité"        -> Pour surveiller les accès (Alice)
  "TaskManager - Business"        -> Tâches créées/complétées par jour (Alice)

────────────────────────────────────────────────────────────────────────────────
RÈGLE 5 - Réunion observabilité hebdomadaire (10 minutes le lundi)
────────────────────────────────────────────────────────────────────────────────

  Agenda (en complément de la réunion SonarQube):
  1. Alice ouvre le dashboard "Vue Générale" sur la plage "Last 7 days"
  2. Tour rapide des métriques (5 min):
     - Nombre de requêtes: en hausse/baisse?
     - Taux d'erreur: stable/acceptable?
     - Durée moyenne: dégradation?
     - Nouvelles alertes cette semaine?
  3. Investigation des anomalies si nécessaire (5 min):
     - Qui a poussé quoi quand l'erreur a eu lieu?
     - Les logs confirment-ils l'hypothèse?
  4. Objectifs de la semaine (optionnel):
     Exemple: "Réduire le taux de 404 de 5.3% à moins de 3%"


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 9 - RÉSUMÉ VISUEL - QUI FAIT QUOI ET QUAND (ELK)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ALICE (Lead / Admin ELK)
  ──────────────────────────
  [OK] Déploie la stack ELK avec docker-compose (une fois)
  [OK] Configure Logstash (pipeline de transformation)
  [OK] Configure Kibana (index pattern, data views)
  [OK] Crée les dashboards principaux (une fois, évolue avec le projet)
  [OK] Configure les alertes (règles d'alerte email)
  [OK] Met en place l'ILM (rétention automatique des logs)
  [OK] Crée les comptes Kibana pour Bob et Claire
  [OK] Fait la revue hebdomadaire des métriques dans Kibana

  BOB (Développeur - utilisation active)
  ──────────────────────────────────────
  [OK] Ajoute des logs métier dans son code (event_type, champs contextuels)
  [OK] Vérifie que ses logs arrivent dans Kibana après chaque feature
  [OK] Utilise Discover pour diagnostiquer les bugs de performance
  [OK] Crée des visualisations ad-hoc pour ses investigations

  CLAIRE (Développeuse - tests et sécurité)
  ──────────────────────────────────────────
  [OK] Configure les tests pour ne pas polluer Elasticsearch
  [OK] Ajoute des tests de logging (test_logging.py)
  [OK] Surveille les patterns suspects dans les logs (404 anormaux)
  [OK] Documente les problèmes trouvés via les commentaires dans les issues


  FLUX DE DONNÉES COMPLET:
  ────────────────────────

  Dev local (Bob/Claire)          Serveur partagé (Alice)
  ──────────────────────          ───────────────────────
  Flask app
    │
    ├─ python-logstash ──TCP──────-> Logstash:5000
    │                                    │
    └─ logs/taskmanager.json             │
         │                               │
         └─ Filebeat ──Beats:5044──────-> Logstash:5044
                                         │
                                         [BLACK_DOWN-POINTING_TRIANGLE] (filtre + enrichissement)
                                   Elasticsearch:9200
                                    (index journalier)
                                         │
                                         [BLACK_DOWN-POINTING_TRIANGLE]
                                   Kibana:5601
                                   (dashboards, alertes)
                                         │
                                         [BLACK_DOWN-POINTING_TRIANGLE]
                               Alice / Bob / Claire
                              (visualisation, analyse)


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PARTIE 10 - CHEATSHEET ELK (RÉFÉRENCE RAPIDE ÉQUIPE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  COMMANDES DOCKER ELK:
  ─────────────────────

  # Démarrer la stack ELK:
  cd taskmanager/docker/
  docker-compose -f docker-compose.elk.yml up -d

  # Arrêter la stack (données conservées):
  docker-compose -f docker-compose.elk.yml down

  # Arrêter ET supprimer toutes les données:
  docker-compose -f docker-compose.elk.yml down -v

  # Voir les logs d'un service:
  docker-compose -f docker-compose.elk.yml logs -f elasticsearch
  docker-compose -f docker-compose.elk.yml logs -f logstash
  docker-compose -f docker-compose.elk.yml logs -f kibana

  # Redémarrer un service:
  docker restart logstash
  docker restart kibana

  # Voir l'état de tous les services:
  docker-compose -f docker-compose.elk.yml ps

  URLS UTILES:
  ────────────

  # Kibana (dashboards et analyse):
  http://sonar.equipe.local:5601

  # Dashboard vue générale:
  http://sonar.equipe.local:5601/app/dashboards

  # Discover (recherche de logs):
  http://sonar.equipe.local:5601/app/discover

  # Dev Tools (console API Elasticsearch):
  http://sonar.equipe.local:5601/app/dev_tools

  # API Elasticsearch directe:
  http://sonar.equipe.local:9200

  # Santé du cluster:
  http://sonar.equipe.local:9200/_cluster/health?pretty

  # Index existants:
  http://sonar.equipe.local:9200/_cat/indices/taskmanager-logs-*?v

  REQUÊTES KQL LES PLUS UTILISÉES:
  ─────────────────────────────────

  # Toutes les erreurs:
  level: "ERROR"

  # Erreurs HTTP:
  http_status >= 400

  # Requêtes lentes:
  duration_ms > 500

  # Route spécifique:
  http_path: "/tasks/search"

  # Événements métier:
  event_type: "task_created"
  event_type: "task_completed"
  event_type: "task_deleted"

  # Sécurité:
  security_alert: true
  event_type: "suspicious_activity"

  # Combinaison:
  http_status >= 500 AND event_type: "http_request"
  level: "WARNING" AND remote_addr: "192.168.1.99"

  ALIAS PRATIQUES (à ajouter dans ~/.bashrc ou ~/.zshrc):
  ────────────────────────────────────────────────────────

  # Démarrer la stack ELK:
  alias elk-up='cd ~/taskmanager/docker && docker-compose -f docker-compose.elk.yml up -d'

  # Arrêter la stack ELK:
  alias elk-down='cd ~/taskmanager/docker && docker-compose -f docker-compose.elk.yml down'

  # Ouvrir Kibana dans le navigateur:
  alias kibana='open http://localhost:5601'

  # Voir la santé d'Elasticsearch:
  alias es-health='curl -s http://localhost:9200/_cluster/health?pretty'

  # Compter les logs d'aujourd'hui:
  alias es-count='curl -s "http://localhost:9200/taskmanager-logs-$(date +%Y.%m.%d)/_count" | python3 -m json.tool'

  # Lancer l'app Flask avec ELK:
  alias dev='cd ~/taskmanager && export $(grep -v '^#' .env | xargs) && ./run_dev.sh'

  VARIABLES D'ENVIRONNEMENT (.env):
  ──────────────────────────────────

  LOGSTASH_HOST=192.168.1.10        # IP du serveur ELK
  LOGSTASH_PORT=5000                # Port TCP Logstash
  KIBANA_URL=http://192.168.1.10:5601
  ELASTICSEARCH_HOST=192.168.1.10   # Pour elk_queries.py
  ELASTICSEARCH_PORT=9200
  LOG_LEVEL=DEBUG                   # DEBUG en dev, INFO en prod
  APP_ENV=development               # development / staging / production
  LOG_FILE=logs/taskmanager.json    # Fichier log local

================================================================================
FIN DE LA SECTION - ELK STACK EN ÉQUIPE DE 3 DÉVELOPPEURS (FLASK)
================================================================================