Il n'y a rien de plus important que d'avoir un bon niveau d'observabilité du service et cela n'est pas possible sans un stockage fiable et rapide des logs de l'application. L'une des solutions les plus populaires de nos jours est ELK ( E lasticSearch- L ogstash- K ibana), mais elle n'est pas aussi universelle qu'il y paraît.
ELK est une solution flexible, pratique et complexe pour collecter et analyser les journaux. Il s’agit d’un filtrage de requêtes évolutif, robuste, flexible et universel. Cependant, l'utilisation d'ELK présente des inconvénients :
Tout cela me fait me demander : existe-t-il une alternative à ELK lorsque nous parlons de journaux ?
Voici ma liste d'exigences pour la solution de gestion des journaux :
Cependant, il est agréable de disposer d'un outil flexible et rapide pour effectuer des analyses régulières et ponctuelles. Soyons plus précis sur le système que je vais mettre en place :
ClickHouse est un système de gestion de base de données SQL hautes performances orienté colonnes pour le traitement analytique en ligne tel qu'il est déclaré.
Les fonctionnalités les plus importantes pour moi étaient :
Vous pouvez trouver le didacticiel ainsi qu'un générateur de journaux sur GitHub .
Tout d'abord, créons docker-compose.yml pour définir les deux services. ( https://github.com/bp72/nginxlogprocessor/blob/init-commit/docker-compose.yml )
version: '3.6' services: ch: image: clickhouse/clickhouse-server container_name: clickhouse restart: always volumes: - clickhousedata:/var/lib/clickhouse/ ports: - '8123:8123' - '9000:9000' ulimits: memlock: soft: -1 hard: -1 nofile: soft: 262144 hard: 262144 # This capabilities prevents Docker from complaining about lack of those cap_add: - SYS_NICE - NET_ADMIN - IPC_LOCK volumes: clickhousedata:
Lançons-le et vérifions si tout fonctionne : connectez-vous à l'instance ClickHouse à l'aide du client ClickHouse à partir du conteneur Docker et vérifiez s'il est disponible via l'hôte local et le port.
> docker-compose up -d [+] Running 3/3 ⠿ Network nginxlogprocessor_default Created 0.1s ⠿ Container clickhouse Started 0.6s
Pour me connecter à l'instance que je préfère, le client ClickHouse
docker-compose exec ch clickhouse-client ClickHouse client version 23.9.1.1854 (official build). Connecting to localhost:9000 as user default. Connected to ClickHouse server version 23.9.1 revision 54466. a8c8da069d94 :)
Maintenant, lorsque tout est défini, créons des bases de données et des tables : une base de données pour suivre les journaux traités et une base de données pour un service spécifique, par exemple nginx , et la première table nginx.access .
L'un des avantages significatifs de ClickHouse est la syntaxe SQL pour les définitions et les requêtes. Ce n'est pas strictement le standard SQL, mais il s'en rapproche beaucoup.
CREATE DATABASE IF NOT EXISTS nginx CREATE DATABASE IF NOT EXISTS logs CREATE TABLE IF NOT EXISTS nginx.access ( reqid String, ts DateTime64(3), level Enum(''=0, 'debug'=1, 'info'=2, 'warn'=3 ,'error'=4), domain String, uri String, ua String, ref String, is_bot Boolean, is_mobile Boolean, is_tablet Boolean, is_pc Boolean, client String, duration Float32, response_code UInt16, addrIPv4 Nullable(IPv4), addrIPv6 Nullable(IPv6), upstream_connect_time Float32, upstream_header_time Float32, upstream_response_time Float32 ) ENGINE MergeTree PRIMARY KEY reqid ORDER BY reqid CREATE TABLE IF NOT EXISTS logs.logfiles ( filename String ) ENGINE MergeTree PRIMARY KEY filename ORDER BY filename
En regardant de plus près l'instruction CREATE TABLE , vous pouvez voir des types légèrement différents et des types complètement nouveaux comme Enum et IPv4. ClickHouse essaie de réduire l'utilisation des ressources et pour ce faire, il l'optimise avec des fonctionnalités intéressantes comme Enum. Il s'agit essentiellement d'un mappage clé-valeur d'une chaîne en un entier de 8 bits ou 16 bits, qui convertit automatiquement lors de l'insertion, il convertit la valeur de la chaîne en int, et lors de la sélection, convertit dans le sens inverse ( lien ).
IPv4, IPv6 sont les types spéciaux pour stocker les adresses de la manière la plus optimale (en tant qu'entier non signé) et les représenter de manière lisible par l'homme, donc fondamentalement, au moment de l'insertion, vous fournissez une représentation sous forme de chaîne de l'adresse IP et ClickHouse fait tout pour vous : le stocke sous forme d'int et de serveur décompressé lors de la sélection.
L'idéologie de ClickHouse est d'insérer rapidement. Pour ce faire, ClickHouse gère mieux les insertions par lots qu'une par une.
Le script d'insertion n'est donc pas très compliqué. La fonction readFile génère des morceaux de données de 50 000 enregistrements au maximum que le client ClickHouse peut insérer. Chaque élément de bloc représente la liste de valeurs relatives aux noms de colonnes dans la liste des colonnes
# it's not an actual code. # the working implementation you can find at https://github.com/bp72/nginxlogprocessor import clickhouse_connect from config import CLICKHOUSE_HOST, CLICKHOUSE_PORT from log import log client = clickhouse_connect.get_client(host=CLICKHOUSE_HOST, port=CLICKHOUSE_PORT) def loadToClickHouse(client, chunk): cols = [ 'reqid', 'ts', 'level', 'domain', 'uri', 'ua', 'ref', 'is_bot', 'is_mobile', 'is_tablet', 'is_pc', 'client', 'duration', 'response_code', 'addrIPv4', 'addrIPv6', 'upstream_connect_time', 'upstream_header_time', 'upstream_response_time', ] client.insert('nginx.access', chunk, column_names=cols) def processFeed(feed, client, chunk_size=10_000): total = 0 for chunk in readFile(feed, chunk_size=chunk_size): total += len(chunk) loadToClickHouse(client, chunk=chunk) log.info(f'process {feed=} inserted={len(chunk)} {total=}')
L'exécution et le timing réels que j'ai sur mon PC, vous pouvez voir que l'analyse et l'insertion du fichier de 800 000 enregistrements ont pris 21 secondes de temps d'exécution Python. Pas mal!
> .venv/bin/python ./main.py I:2023-10-15 12:44:02 [18764] f=transport.py:1893 Connected (version 2.0, client OpenSSH_8.9p1) I:2023-10-15 12:44:02 [18764] f=transport.py:1893 Authentication (publickey) successful! I:2023-10-15 12:44:02 [18764] f=fetcher.py:14 connect host='*.*.*.*' port=22 user='root' password=None I:2023-10-15 12:44:02 [18764] f=fetcher.py:18 run cmd='ls /var/log/nginx/*access*.log-*' I:2023-10-15 12:44:02 [18764] f=fetcher.py:34 download src=/var/log/nginx/access.log-2023100812.gz dst=/tmp/access.log-2023100812.gz I:2023-10-15 12:44:07 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=50000 I:2023-10-15 12:44:08 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=100000 I:2023-10-15 12:44:10 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=150000 I:2023-10-15 12:44:11 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=200000 I:2023-10-15 12:44:13 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=250000 I:2023-10-15 12:44:14 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=300000 I:2023-10-15 12:44:15 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=350000 I:2023-10-15 12:44:17 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=400000 I:2023-10-15 12:44:18 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=450000 I:2023-10-15 12:44:20 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=500000 I:2023-10-15 12:44:21 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=550000 I:2023-10-15 12:44:23 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=600000 I:2023-10-15 12:44:24 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=650000 I:2023-10-15 12:44:25 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=700000 I:2023-10-15 12:44:27 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=750000 I:2023-10-15 12:44:28 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=50000 total=800000 I:2023-10-15 12:44:28 [18764] f=main.py:20 process feed='/tmp/access.log-2023100812.gz' inserted=2190 total=802190 I:2023-10-15 12:44:28 [18764] f=fetcher.py:34 download src=/var/log/nginx/access.log-2023100814.gz dst=/tmp/access.log-2023100814.gz I:2023-10-15 12:44:31 [18764] f=main.py:20 process feed='/tmp/access.log-2023100814.gz' inserted=50000 total=50000 I:2023-10-15 12:44:32 [18764] f=main.py:20 process feed='/tmp/access.log-2023100814.gz' inserted=50000 total=100000 I:2023-10-15 12:44:33 [18764] f=main.py:20 process feed='/tmp/access.log-2023100814.gz' inserted=30067 total=130067
ClickHouse utilise SQL pour interroger la base de données, ce qui est très confortable pour la plupart des ingénieurs logiciels et intuitivement simple.
Commençons par vérifier le nombre d'enregistrements dont nous disposons, il est de 22 millions.
a8c8da069d94 :) select count(1) from nginx.access; SELECT count(1) FROM nginx.access Query id: f94881f3-2a7d-4039-9646-a6f614adb46c ┌──count()─┐ │ 22863822 │ └──────────┘
Il est facile d'avoir des requêtes avec différentes pannes, ce qui peut être utile pour la détection et la solution des problèmes, par exemple, j'aimerais savoir à partir de quelle adresse IP l'hôte est analysé pour détecter les vulnérabilités.
Cette requête montre à quel point les requêtes de données flexibles peuvent être comparées à l'ELK. L'instruction WITH .. AS et IN / NOT IN, la sous-requête, l'agrégation et la filtration rendent ClickHouse très pratique.
a8c8da069d94 :) with baduri as (select uri, count(1) from nginx.access where response_code = 404 and uri not in ('/about/', '/favicon.ico') group by 1 having count(1) > 3 order by 2 desc limit 10) select IPv4NumToStringClassC(addrIPv4), count(1) from nginx.access where uri in (select uri from baduri) and addrIPv4 is not null group by 1 order by 2 desc limit 5 WITH baduri AS ( SELECT uri, count(1) FROM nginx.access WHERE (response_code = 404) AND (uri NOT IN ('/about/', '/favicon.ico')) GROUP BY 1 HAVING count(1) > 3 ORDER BY 2 DESC LIMIT 10 ) SELECT IPv4NumToStringClassC(addrIPv4), count(1) FROM nginx.access WHERE (uri IN ( SELECT uri FROM baduri )) AND (addrIPv4 IS NOT NULL) GROUP BY 1 ORDER BY 2 DESC LIMIT 5 Query id: cf9bea33-212b-4c58-b6af-8e0aaae50b83 ┌─IPv4NumToStringClassC(addrIPv4)─┬─count()─┐ │ 8.219.64.xxx │ 961 │ │ 178.128.220.xxx │ 378 │ │ 103.231.78.xxx │ 338 │ │ 157.245.200.xxx │ 324 │ │ 116.203.28.xxx │ 260 │ └─────────────────────────────────┴─────────┘ 5 rows in set. Elapsed: 0.150 sec. Processed 45.73 million rows, 1.81 GB (303.88 million rows/s., 12.01 GB/s.) Peak memory usage: 307.49 MiB.
Obtenons le top 5 des URI les plus populaires par domaine. Cette requête utilise la fonction pratique LIMIT x BY <field>.
a8c8da069d94 :) select domain, uri, count(1) from nginx.access where domain in ('example.com', 'nestfromthebest.com', 'az.org') group by 1, 2 order by 1, 3 desc limit 5 by domain SELECT domain, uri, count(1) FROM nginx.access WHERE domain IN ('example.com', 'nestfromthebest.com', 'az.org') GROUP BY 1, 2 ORDER BY 1 ASC, 3 DESC LIMIT 5 BY domain Query id: 2acd328c-ed82-4d36-916b-8f2ecf764a9d ┌─domain──────┬─uri────────────┬─count()─┐ │ az.org │ /about/ │ 382543 │ │ az.org │ /contacts/ │ 42066 │ │ az.org │ /category/id7 │ 2722 │ │ az.org │ /category/id14 │ 2704 │ │ az.org │ /category/id2 │ 2699 │ │ example.com │ /about/ │ 381653 │ │ example.com │ /contacts/ │ 42023 │ │ example.com │ /category/id2 │ 2694 │ │ example.com │ /category/id8 │ 2688 │ │ example.com │ /category/id13 │ 2670 │ └─────────────┴────────────────┴─────────┘ ┌─domain──────────────┬─uri────────────┬─count()─┐ │ nestfromthebest.com │ /about/ │ 383377 │ │ nestfromthebest.com │ /contacts/ │ 42100 │ │ nestfromthebest.com │ /category/id8 │ 2726 │ │ nestfromthebest.com │ /category/id14 │ 2700 │ │ nestfromthebest.com │ /category/id4 │ 2696 │ └─────────────────────┴────────────────┴─────────┘ 15 rows in set. Elapsed: 0.062 sec. Processed 23.97 million rows, 918.43 MB (388.35 million rows/s., 14.88 GB/s.) Peak memory usage: 98.67 MiB.
ClickHouse est un excellent outil pour stocker et manipuler des données spécifiques telles que des journaux à grande échelle. Cela vaut vraiment la peine d'approfondir votre apprentissage et de comprendre, par exemple, la structure des données imbriquées, les outils d'échantillonnage, les fonctions de fenêtre, etc.
J’espère que vous avez apprécié ce petit article et qu’il vous a été utile !