Présentation
Cet outil permet de gérer automatiquement la rotation des ficheirs journal afin de limiter leur taille et leur nombre. Inspiré du mécanisme de gestion des journaux utilisé par le NameNode de YARN, il garantit que le fichier principal contient toujours les entrées les plus récentes, tandis que les anciens journaux sont répartis dans des fichiers numérotés de manière décroissante par ancienneté.
Caractéristiques principales
- Compatible avec les flux d'entrée standard (pipe) sous Linux
- Contrôle de la taille maximale de chaque fichier journal
- Limitation du nombre total de fichiers conservés
- Maintien de l'ordre chronologiuqe des fichiers avec suffixe numérique
- Faible consommation mémoire
- Dépendances minimales : uniquement Python 2.7+
Code source
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
import sys
import os
ERR_OPEN = 301
ERR_WRITE = 302
ERR_RENAME = 303
ERR_DELETE = 304
ERR_PATH = 305
UNIT_MULTIPLIERS = {
'B': 1,
'K': 1024,
'M': 1024 ** 2,
'G': 1024 ** 3
}
def parse_size_limit(raw_value):
last_char = raw_value[-1].upper()
if last_char.isdigit():
return int(raw_value)
coefficient = int(raw_value[:-1])
multiplier = UNIT_MULTIPLIERS.get(last_char, 1)
return coefficient * multiplier
class CircularLogRotator(object):
def __init__(self, max_count, size_cap, base_path):
self.max_files = int(max_count)
self.byte_limit = parse_size_limit(size_cap)
self.base_path = base_path
self.dir_path = os.path.realpath(os.path.dirname(base_path))
self.current_handle = None
self.bytes_written = 0
self._initialize_stream()
def _build_path(self, index=None):
if index is None:
return self.base_path
return "{base}.{idx}".format(base=self.base_path, idx=index)
def _initialize_stream(self):
try:
if os.path.exists(self.base_path):
with open(self.base_path, 'r') as reader:
self.bytes_written = len(reader.read())
else:
self.bytes_written = 0
self.current_handle = open(self.base_path, 'ab+')
self.current_handle.write('')
except IOError:
sys.exit(ERR_OPEN)
def _find_available_slot(self):
for slot in range(1, self.max_files):
candidate = self._build_path(slot)
if not os.path.exists(candidate):
return slot
return self.max_files - 1
def _shift_files(self, target_slot):
try:
if target_slot == self.max_files - 1:
oldest = self._build_path(target_slot)
if os.path.exists(oldest):
os.remove(oldest)
except OSError:
sys.exit(ERR_DELETE)
try:
if target_slot is not None:
for pos in range(target_slot, 1, -1):
src = self._build_path(pos - 1)
dst = self._build_path(pos)
os.rename(src, dst)
os.rename(self.base_path, self._build_path(1))
except OSError:
sys.exit(ERR_RENAME)
def _perform_rotation(self):
slot = self._find_available_slot()
self._shift_files(slot)
self.current_handle = open(self.base_path, 'ab+')
self.bytes_written = 0
def ingest(self, data=''):
payload_size = len(data)
try:
self.current_handle.write(data)
self.bytes_written += payload_size
if self.bytes_written >= self.byte_limit:
self.current_handle.flush()
self.current_handle.close()
self._perform_rotation()
except IOError:
sys.exit(ERR_WRITE)
def main():
parser = argparse.ArgumentParser(
description='Outil de rotation des journaux en mode pipe.'
)
parser.add_argument(
'-n', '--max-files',
dest='max_count',
type=int,
default=2,
help='Nombre maximal de fichiers journal conservés (defaut: %(default)s)'
)
parser.add_argument(
'base_path',
type=str,
help='Chemin complet vers le fichier journal principal'
)
parser.add_argument(
'size_cap',
type=str,
help='Taille maximale par fichier (ex: 100M, 1G, 500K)'
)
args = parser.parse_args()
rotator = CircularLogRotator(args.max_count, args.size_cap, args.base_path)
for entry in sys.stdin:
rotator.ingest(entry)
if __name__ == '__main__':
main()
Prérequis
Python version 2.7 ou supérieure.
Utilisation
chmod +x /chemin/vers/rotatelogs.py
votre_commande | ./rotatelogs.py -n 5 /var/log/mon_application.log 100M
Exemple de test
#!/bin/bash
set -e
ITERATIONS=20
DEBUT=$(date +"%s")
rm -f ./journal_test.log*
for i in $(seq 1 $ITERATIONS); do
echo "Ligne de test numero $i"
done | python ./rotatelogs.py -n 3 ./journal_test.log 500M
FIN=$(date +"%s")
TEMPS=$(echo "(${FIN}-${DEBUT})/${ITERATIONS}" | bc -l)
echo "Temps moyen par iteration : ${TEMPS}s"
Résultats observés
| Commande | Temps écoulé |
|---|---|
| Extraction de 30 000 lignes (head) | 13s |
| Traitement complet (6,1 Go) | 136s |
| Extraction de 30 000 lignes (tail) | 66s |
| Apache rotatelogs -n 3 500M | 102s |
| Cet outil -n 5 100M | 60s |
| Cet outil -n 3 500M | 56s |
Structure des fichiers générés
-rw-r--r-- 1 root root 243M journal.log
-rw-r--r-- 1 root root 501M journal.log.1
-rw-r--r-- 1 root root 501M journal.log.2
Le fichier principal journal.log contient les données les plus récentes. Les fichiers numérotés contiennnet les anciens journaux, le suffixe le plus élevé correspondant aux entrées les plus anciennes.
Références
Documentation officielle d'Apache : rotatelogs - Programme de rotation des journaux Apache