Logstash pour les devs - 14 - Le filtre Ruby : quand les autres ne suffisent pas

Écrire du code Ruby inline dans un pipeline Logstash pour des transformations complexes. Puissant mais a utiliser avec parcimonie.

  1. 01 Logstash pour les devs - 00 - Pourquoi Logstash existe encore en 2026
  2. 02 Logstash pour les devs - 01 - L'Elastic Stack de A a Z
  3. 03 Logstash pour les devs - 02 - Installer Logstash avec Docker en 5 minutes
  4. 04 Logstash pour les devs - 03 - Anatomie d'un pipeline Logstash
  5. 05 Logstash pour les devs - 04 - Inputs stdin et file : lire des donnees locales
  6. 06 Logstash pour les devs - 05 - Input Beats : recevoir des logs de Filebeat
  7. 07 Logstash pour les devs - 06 - Inputs HTTP, TCP et UDP : recevoir des donnees réseau
  8. 08 Logstash pour les devs - 07 - Inputs Kafka et JDBC : sources avancees
  9. 09 Logstash pour les devs - 08 - Les codecs : decoder et encoder les donnees
  10. 10 Logstash pour les devs - 09 - Le filtre Grok : parser n'importe quel log
  11. 11 Logstash pour les devs - 10 - Le filtre Dissect : parser sans regex
  12. 12 Logstash pour les devs - 11 - Le filtre Mutate : transformer les champs
  13. 13 Logstash pour les devs - 12 - Filtres Date et GeoIP : temps et geolocalisation
  14. 14 Logstash pour les devs - 13 - Filtres KV, JSON et XML : parser les formats structures
  15. 15 Logstash pour les devs - 14 - Le filtre Ruby : quand les autres ne suffisent pas
  16. 16 Logstash pour les devs - 15 - Filtres Aggregate et Metrics : correler les événements
  17. 17 Logstash pour les devs - 16 - Conditionnels et contrôle de flux
  18. 18 Logstash pour les devs - 17 - Output Elasticsearch : envoyer les donnees
  19. 19 Logstash pour les devs - 18 - Outputs file, stdout et les autres
  20. 20 Logstash pour les devs - 19 - Gerer le multiline : stack traces et logs multi-lignes
  21. 21 Logstash pour les devs - 20 - Pipelines multiples et pipeline-to-pipeline
  22. 22 Logstash pour les devs - 21 - Performance et tuning Logstash
  23. 23 Logstash pour les devs - 22 - Monitoring Logstash : metriques et alertes
  24. 24 Logstash pour les devs - 23 - Dead Letter Queue : ne plus perdre d'événements
  25. 25 Logstash pour les devs - 24 - Sécurité Logstash : SSL, auth et secrets
  26. 26 Logstash pour les devs - 25 - Debugger un pipeline Logstash
  27. 27 Logstash pour les devs - 26 - Tester ses pipelines avant la prod
  28. 28 Logstash pour les devs - 27 - Cas pratique : centraliser des logs applicatifs
  29. 29 Logstash pour les devs - 28 - Cas pratique : ETL avec Logstash et PostgreSQL
  30. 30 Logstash pour les devs - 29 - Cas pratique : enrichir des donnees en temps réel
  31. 31 Logstash pour les devs - 30 - Logstash en production : architecture et bonnes pratiques
  32. 32 Logstash pour les devs - 31 - Glossaire Logstash de A a Z

14 - Le filtre Ruby : quand les autres ne suffisent pas

Ce que tu vas apprendre

  • La syntaxe du filtre Ruby (inline et fichier externe)
  • L'API Event pour lire, écrire et supprimer des champs
  • Des cas d'usage concrets : calcul de duree, logique conditionnelle complexe, aplatissement de structures
  • Les impacts sur la performance
  • Quand le filtre Ruby est justifie et quand il ne l'est pas

Prerequisites

  • Connaitre les filtres de base (Grok, Mutate, Date)
  • Des bases en Ruby aident, mais le code reste simple

Le dernier recours

Je dis toujours a mes collegues : si tu utilises le filtre Ruby, c'est que tu n'as pas trouve le bon plugin. Et souvent, c'est vrai. Mais parfois, aucun plugin ne fait exactement ce dont tu as besoin. Tu veux calculer la différence entre deux timestamps, tronquer un champ a 500 caractères, ou faire un lookup dans un hash en mémoire. Les plugins standards ne couvrent pas tous ces cas.

Le filtre Ruby te donne acces a un vrai langage de programmation a l'intérieur du pipeline. C'est puissant, mais ca a un coût en performance.

Syntaxe inline

filter {
  ruby {
    code => '
      level = event.get("level")
      if level == "ERROR" || level == "FATAL"
        event.set("alert", true)
        event.set("[@metadata][notify]", "slack")
      end
    '
  }
}

Le code Ruby est entre guillemets simples. L'objet event represente l'événement courant. Tu lis avec event.get, tu ecris avec event.set.

L'API Event

Lire un champ

ruby# Champ de premier niveau
level = event.get("level")

# Champ imbrique
city = event.get("[geoip][city_name]")

# Metadata
beat = event.get("[@metadata][beat]")

Écrire un champ

ruby# Creer ou ecraser un champ
event.set("processed", true)

# Champ imbrique
event.set("[metrics][p99]", 234.5)

# Metadata
event.set("[@metadata][target_index]", "logs-critical")

Supprimer un champ

rubyevent.remove("temporary_field")
event.remove("[nested][field]")

Ajouter un tag

rubyevent.tag("slow_request")

Annuler un événement

rubyevent.cancel

L'événement est supprime du pipeline. Il ne passera pas aux filtres suivants et ne sera pas envoye a l'output. Utile pour filtrer les événements qu'on ne veut pas indexer.

Cas d'usage 1 : calculer une duree

Tu as deux timestamps dans l'événement : le début et la fin d'une requête. Tu veux la duree en millisecondes.

filter {
  ruby {
    code => '
      start_str = event.get("request_start")
      end_str = event.get("request_end")

      if start_str && end_str
        start_time = Time.parse(start_str)
        end_time = Time.parse(end_str)
        duration_ms = ((end_time - start_time) * 1000).round
        event.set("duration_ms", duration_ms)
      end
    '
  }
}

Aucun plugin standard ne fait ce calcul. Le filtre date parse un timestamp, mais ne calcule pas de différence.

Cas d'usage 2 : tronquer un champ

Les champs de message trop longs font exploser l'index Elasticsearch. Limite a 1000 caractères :

filter {
  ruby {
    code => '
      msg = event.get("message")
      if msg && msg.length > 1000
        event.set("message", msg[0..999])
        event.set("message_truncated", true)
      end
    '
  }
}

Cas d'usage 3 : aplatir un objet imbrique

Tu recois un JSON imbrique et tu veux aplatir les champs pour Elasticsearch :

json{
  "request": {
    "headers": {
      "content-type": "application/json",
      "authorization": "Bearer xxx"
    }
  }
}

Tu veux request_headers_content_type au lieu de [request][headers][content-type] :

filter {
  ruby {
    code => '
      def flatten_hash(hash, prefix = "")
        result = {}
        hash.each do |key, value|
          new_key = prefix.empty? ? key : "#{prefix}_#{key}"
          new_key = new_key.gsub("-", "_")
          if value.is_a?(Hash)
            result.merge!(flatten_hash(value, new_key))
          else
            result[new_key] = value
          end
        end
        result
      end

      request = event.get("request")
      if request.is_a?(Hash)
        flat = flatten_hash(request, "request")
        flat.each { |k, v| event.set(k, v) }
        event.remove("request")
      end
    '
  }
}

Cas d'usage 4 : filtrer les événements

Supprimer les health checks du pipeline :

filter {
  ruby {
    code => '
      url = event.get("url") || ""
      method = event.get("http_method") || ""

      if url == "/health" || url == "/ready" || url == "/metrics"
        event.cancel
      end
    '
  }
}

Tu pourrais aussi faire ca avec un conditionnel Logstash + drop {}, mais le filtre Ruby permet une logique plus complexe (regex, listes, etc.).

Le bloc init

Si tu as besoin d'initialiser des variables une seule fois (pas a chaque événement) :

filter {
  ruby {
    init => '
      require "set"
      @ignore_paths = Set.new(["/health", "/ready", "/metrics", "/favicon.ico"])
      @sensitive_fields = ["password", "token", "secret", "authorization"]
    '
    code => '
      # Filtrer les paths a ignorer
      url = event.get("url") || ""
      event.cancel if @ignore_paths.include?(url)

      # Masquer les champs sensibles
      @sensitive_fields.each do |field|
        if event.get(field)
          event.set(field, "[REDACTED]")
        end
      end
    '
  }
}

Le bloc init s'exécuté une seule fois au chargement du pipeline. Les variables d'instance (@variable) persistent entre les événements. Le bloc code s'exécuté pour chaque événement.

Fichier externe

Pour du code plus long, mets-le dans un fichier :

ruby# logstash/scripts/enrich.rb

def register(params)
  @lookup = {
    "api-users" => "team-backend",
    "api-orders" => "team-backend",
    "api-payments" => "team-payments",
    "web-frontend" => "team-frontend"
  }
end

def filter(event)
  service = event.get("service")
  if service && @lookup.key?(service)
    event.set("team", @lookup[service])
  else
    event.set("team", "unknown")
  end
  return [event]
end

Utilisation dans le pipeline :

filter {
  ruby {
    path => "/usr/share/logstash/scripts/enrich.rb"
  }
}

Le fichier doit définir au minimum la méthode filter(event) qui renvoie un tableau d'événements. register(params) est l'équivalent de init pour les fichiers externes.

Monte le dossier scripts dans Docker :

yamllogstash:
  volumes:
    - ./logstash/scripts/:/usr/share/logstash/scripts/:ro

Structure du projet :

logstash/
├── config/
│   ├── logstash.yml
│   └── pipelines.yml
├── pipeline/
│   └── main.conf
├── patterns/
│   └── custom
└── scripts/
    └── enrich.rb

Performance

Le filtre Ruby est lent. Pas catastrophiquement lent, mais sensiblement plus lent que les plugins natifs.

Opération Plugin natif Filtre Ruby
Renommer un champ mutate: ~0.01 ms ruby: ~0.1 ms
Convertir un type mutate: ~0.01 ms ruby: ~0.1 ms
Logique conditionnelle if/else: ~0.01 ms ruby: ~0.15 ms

Le facteur est d'environ 10x. Pour 1000 événements par seconde, c'est invisible. Pour 50 000 par seconde, ca s'additionne.

Regles :

  1. Si un plugin natif fait le job, utilise-le
  2. Si tu as besoin de Ruby, minimise le code dans code et mets les initialisations dans init
  3. Evite les require dans code (mets-les dans init)
  4. Ne fais pas d'I/O (HTTP, fichier) dans le filtre Ruby, c'est un bottleneck

Quand Ruby est justifie

  • Calcul de duree entre deux timestamps
  • Logique conditionnelle complexe (plus de 3 niveaux d'imbrication)
  • Manipulation de structures de donnees (aplatir, pivoter, regrouper)
  • Lookup dans un dictionnaire en mémoire
  • Masquage de donnees sensibles avec une logique non triviale
  • Filtrage d'événements avec des regles complexes

Quand Ruby n'est PAS justifie

  • Renommer un champ (mutate rename)
  • Convertir un type (mutate convert)
  • Ajouter un champ statique (mutate add_field)
  • Parser du JSON (filtre json)
  • Geolocaliser une IP (filtre geoip)

Si tu te retrouves a reecrire un plugin existant en Ruby, arrêté et utilise le plugin. Sur paltemps.fr, on a une regle : tout filtre Ruby doit avoir un commentaire qui explique pourquoi un plugin natif ne suffit pas.

Résumé

  • Le filtre Ruby donne acces a un vrai langage de programmation dans le pipeline
  • L'API Event : event.get, event.set, event.remove, event.cancel, event.tag
  • Le bloc init s'exécuté une fois, code s'exécuté par événement
  • Pour du code long, utilise un fichier externe avec path
  • Ruby est ~10x plus lent que les plugins natifs, utilise-le avec parcimonie
  • Justifie toujours l'usage de Ruby : si un plugin natif fait le job, préféré-le

Precedent : 13 - Filtres KV, JSON, XML | Suivant : 15 - Aggregate et Metrics

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.