Introduction

Cette partie a pour objet de trouver le moyen d'obtenir le plus d'informations possible des logs issus d'Apache. Ce serveur HTTP est assez verbeux lorsqu'il est bien configuré et on peut obtenir un maximum d'informations sur les clients qui accèdent à ses services.

Contenu des logs

Avant de commencer, étudtions un peu ce qu'on peut récupérer dans un log Apache:

188.165.15.236 - - [06/Sep/2015:06:59:32 +0200] "GET /kb/GIS/generate_osm/ HTTP/1.1" 200 7314 "-" "Mozilla/5.0 (compatible; AhrefsBot/5.0; +http://ahrefs.com/robot/)"

Voici ce qu'on peut récupérer dans l'ordre d'apparition:

  • L'adresse IP qui peut être du type IPv4 ou IPv6.
  • L'identifiant de l'utilisateur si on travaille en mode authentifié (sinon "-").
  • Le type d'authentification de l'utilisateur
  • La date de connexion (avec le timestamp complet). On s'en servira pour donner le timestamp de l'évènement logstash.
  • La requête HTTP qui peut être décomposée en trois éléments:
    • La commande HTTP (GET/POST/PUT/DELETE).
    • La ressource (la page qui a été demandée).
    • Le type de protocole et sa version.
  • Le code de retour du serveur HTTP.
  • Le nombre d'octets envoyés.
  • Le champ qui suit est le referrer, c'est à dire de quelle page on provient.
  • Enfin, on trouve le code du User-Agent du client HTTP.

Au final, cela fait beaucoup de champs différents à gérer, sachant qu'on peut les combiner ensemble pour tenter de répondre aux questions que peuvent se poser tous les webmasters. Essayons de nous limiter aux plus pertinentes (c'est à dire celles qui m'intéressent !).

Configurer des logs apache

Métriques

Logstash vient avec un filtre grok prêt à l'emploi. Ce dernier se nomme %{COMBINEDAPACHELOG} et permet d'extraire tous les champs du log Apache. Mais dans notre cas, il nous faut également ajouter d'autres données. En effet, si vous avez plusieurs virtualhost, Apache n'indique pas dans les logs le domaine virtuel qui a été requêté. Cette information peut être obtenue à partir du nom du fichier, si vous suivez la règle de log présentée ci-dessus dans votre configuration Apache.

Autre information qu'on peut également extraire du nom des fichiers de log, c'est la nature de la connexion: HTTP ou HTTPS. Enfin, il serait bon de savoir si on a une adresse IPv4 ou IPv6. De plus, depuis l'adresse IP, on peut déduire à peu près la provenance du client.

Bien entendu, pour respecter la règle des documents logstash, nous allons utiliser le type "apache" pour distinguer ces données des autres systèmes.

Fichier de configuration de logstash

Voici le filtre pour logstash qui correspond à la configuration cible exposée ci-dessus.

file {
  path => "/home/medspx/projects/ELK/data/apache2/access_*.log"
  start_position => beginning
  type => "apache"
}

filter {
  # Filtre pour les logs d'Apache
  if [type] == "apache" {
    # L'intégralité des données du log est récupérée via le motif déjà existant
    grok {
      match => { "message" => "%{COMBINEDAPACHELOG}" }
    }
    # On récupère la date de l'évènement comme champ interne de Logstash
    date {
      match => [ "timestamp", "dd/MMM/yyyy:HH:mm:ss Z" ]
    }
    # On va traiter l'adresse IP du client via geoip pour avoir plus d'information sur la provenance.
    geoip {
      source => "clientip"
    }
    # On va récupérer le domaine virtuel et le type de connexion depuis le nom du fichier
    grok {
      match => { "path" => "access_%{WORD:virtualhost}_%{WORD:tls}" }
    }
  }
}

Nourrir Logstash

L'idée est assez simple. Elle consiste à préparer une concaténation de fichiers de logs Apache par domaine virtuel et par type de connexion (tls ou plain). Vous pouvez bien entendu mettre quelquechose de plus complexe en oeuvre si vous voulez avoir du quasi temps-réel. Mais ce n'est pas ce que je cherche à faire. Tout au plus, je veux pouvoir allumer ma station de travail, lancer la pile ELK et une fois que c'est fait, rapatrier les logs qui m'intéressent pour les injecter dans Logstash pour pouvoir faire une analyse via Kibana.

Attention, Logstash et Elasticsearch sont quand même des gros bloats en Java (on ne se refait pas !): ils sont particulièrement peu efficaces sur du matériel conventionnel, hors datacenter. Par exemple, le chargement de 73 Mo de logs Apache (1 an des logs de mon serveur auto-hébergé prend près de 40 minutes à logstash pour être digéré complètement. Ensuite, Elasticsearch harmonise ses index et le processus prend 10 minutes supplémentaires...

Une fois les données chargées, le noeud Elasticsearch lors de son lancement initial prend environ 1 minute pour démarrer mais il charge progressivement la RAM qui lui a été affectée pendant près de 10 minutes sur cette même machine...

Reporting

Introduction

Grâce au filtre logstash précédent, nous disposons de très nombreuses variables. Créer l'ensemble des rapports possibles prendrait sans doute un livre entier aussi, j'ai décidé de limiter le reporting aux éléments qui suivent. Retenez bien que Kibana travaille sur un secteur de temps donné et que donc toutes ces mesures sont dépendantes d'une période de temps.

  • Pourcentage d'adresse IPv6/IPv4 par rapport au nombre de connexions sur une période donnée.
  • Quelles sont les 10 pages les plus visitées ?
  • Classement des virtualhosts par ordre de fréquentation.
  • Quelles sont les 50 pages html les plus lourdes ?
  • Ratio connexions HTTPS/HTTP/nombre de connexions ?
  • Classement par pays de l'ensemble des requêtes ?
  • Quelles sont les adresses IP qui accèdent aux services non publics ?
  • Proportion du traffic requêté par des robots/crawlers/lecteurs RSS ?
  • Fréquentation en nombre de hits des pages publiques ?
  • Quelles sont les requêtes qui aboutissent à des codes d'erreur ?

Métrique: 10 pages les plus visitées

[[!img Erreur: bad image filename]]

Ce métrique est très simple. Voici la recette pour le constituer:

  • Filtre: *
  • Visualize: New Pie Chart.
  • Buckets: split slices.
  • Aggregation: Terms
  • Field: request.raw
  • Order:Top, Size: 10.
  • Order by: metric: Count

Métrique: Classement des virtualdomains par ordre de fréquentation

[[!img Erreur: bad image filename]]

Autre métrique assez simple qui travaille sur le champ virtualdomain que nous avons défini dans le filtre Logstash.

  • Filtre: *
  • Visualize: New Pie Chart.
  • Buckets: split slices.
  • Aggregation: Terms
  • Field: virtualdomain.raw
  • Order:Top, Size: 10.
  • Order by: metric: Count

Métrique: Ratio connexions HTTPS/HTTP/nombre de connexions

Pour cette mesure, nous allons utiliser deux diagrammes. Le premier est un camembert qui montre le nombre de requêtes TLS vs clair. Ensuite, nous allons utiliser des barres verticales par virtualdomain que nous allons subdiviser en fonction du critère TLS.

L'attribut utilisé est bien sûr l'attribut tls.raw que nous avons configuré dans notre filtre Logstash.

Camembert:

  • Filtre: *
  • Visualize: New Pie Chart.
  • Buckets: split slices.
  • Aggregation: Terms
  • Field: tls.raw
  • Order:Top, Size: 2.
  • Order by: metric: Count

Barres verticales:

  • Filtre: *
  • Visualize: Vertical Bar Chart.
  • Buckets: X-Axis.
  • Aggregation: Terms
  • Field: virtualdomain.raw
  • Order:Top, Size: 10.
  • Order by: metric: Count
  • Split bars
  • Aggregation: Terms
  • Field: tls.raw
  • Order:Top, Size: 2.
  • Order by: metric: Count

Métrique: Proportion du traffic requêté par des robots/crawlers/lecteurs RSS

Ici, nous allons utiliser des filtres... La syntaxe de ces filtres est assez simple, comme nous allons pouvoir nous en rendre compte.

Nous voulons extraire les connexions issues des lecteurs RSS et des crawlers. On peut repérer ces connexions grâce au champ agent. En règle générale, un lecteur RSS contient au moins la chaîne de caractères 'rss' dans son champ agent. Pour les crawlers, on peut dire la même chose avec la chaîne 'bot'. Donc, nous voulons les agents contenant 'bot' d'un côté, les agents contenant 'rss' de l'autre et aussi toutes les connexions autres.

Filtrer sur 'bot' sur le champ agent donne lieu à la syntaxe suivante: agent:*bot*. Pour les lecteurs RSS, on trouve bien entendu: agent:*rss*. Pour tout le reste, il suffit de prendre l'inverse. Cela donne la syntaxe suivante: NOT agent:*bot* AND NOT agent:*rss* !

Pour créer le métrique, nous allons employer un camembert avec trois catégories: bot, rss et le reste. Pour cela, voici la recette:

  • Filtre: *
  • Visualize: New Pie Chart.
  • Buckets: split slices.
  • Aggregation: Filters
  • Filtre 1: agent:bot
  • Filtre 2: agent:rss
  • Filtre 3: NOT agent:bot AND NOT agent:rss

Et voilà ! Rien de bien complexe...

Métrique: Fréquentation en nombre de hits des pages publiques

Sur mon serveur, j'ai tout un tas de services non publics. Si je veux savoir qui regarde mes pages publiques, il faut que j'enlève les pages privées.

Voici une syntaxe de filtre simple (quoiqu'un peu longue): NOT (request:news OR request:posteweb OR request:admin OR request:*css* OR request:favicon.ico OR request:*index.rss* OR request:robots.txt OR request:\/tag\/* OR request:*calendar*)

Voici la recette à appliquer:

  • Filtre: NOT (request:news OR request:posteweb OR request:admin OR request:css OR request:favicon.ico OR request:index.rss OR request:robots.txt OR request:\/tag\/* OR request:calendar)
  • Visualize: Vertical Bar Chart.
  • Buckets: X-Axis.
  • Aggregation: Terms
  • Field: request.raw
  • Order:Top, Size: 10.
  • Order by: metric: Count

Métrique: Quelles sont les requêtes qui aboutissent à des codes d'erreur ?

Ici aussi, nous allons utiliser des filtres. Tout ce qu'on veut, c'est récupérer les requêtes en erreur, dont le code HTTP n'est pas 200.

Voici la recette:

  • Filtre: NOT response:200
  • Visualize: Vertical Bar Chart.
  • Buckets: X-Axis.
  • Aggregation: Terms
  • Field: request.raw
  • Order:Top, Size: 15.
  • Order by: metric: Count
  • Split bars
  • Aggregation: Significant Terms
  • Field: response.raw
  • Size: 10.