
Парсинг логов – типовая задача для системных администраторов и девопсов. Обычно это регулярная операция. Данные постоянно пишутся в конец файла, поэтому периодически его надо открывать и обрабатывать новые записи. В процессе нужно искать место, где мы закончили чтение в прошлый раз. Давайте автоматизируем этот поиск так, чтобы он не требовал просмотра лога с самого начала.
Первое, что приходит на ум, – запомнить позицию в файле, на которой мы закончили. Её возвращает функция tell. На следующей итерации перейдём сразу на эту позицию и продолжим чтение оттуда. Это делает функция seek практически бесплатно.
Схема простая и рабочая, но при ротации лога возникают проблемы. Во-первых, переход за границу файла не приведёт к ошибке, просто при чтении мы получим пустые данные. Значит, перед переходом надо проверить, что сохранённое значение не превышает размер файла, и если это не так, читать лог с начала. Даже в таком случае после ротации может записатся достаточно данных для успешного прохождения проверки. Тогда мы как ни в чём не бывало продолжим «с места, где закончили» и пропустим кусок данных.
Есть способ усовершенствовать алгоритм: запоминать смещение не конца файла, а начала последней прочитанной строки и её значение. В этом случае мы также перейдём на закешированную позицию, а потом прочитаем строку и сравним её с сохранённой. Если значения совпали, продолжаем чтение дальше. Если различаются, возвращаемся к началу файла. Так как в каждой строке лога обычно есть метка времени, успешное прохождение проверки после ротации практически невозможно.
Я реализовал эту идею в небольшом Python модуле. Он называется cutthelog
. В нём есть единственный класс CutTheLog
, позволяющий открыть файл и выполнить описанную проверку. Как результат он возвращает итератор по непросмотренным строкам лога. Для создания объекта передаём путь файла и опционально смещение и значение строки, на которой мы закончили чтение. Выглядит это так:
import sys
from cutthelog import CutTheLog
ctl = CutTheLog('/var/log/syslog', offset=7777, last_line=b'...')
with ctl as line_iter:
print('Starting at:', ctl.get_position(), end='', file=sys.stderr)
for line in line_iter:
print(line.decode(), end='')
print('Ending at:', ctl.get_position(), end='', file=sys.stderr)
Строки лога имеют тип bytes
, так как даже на моей дескпотной убунте в логе /var/log/syslog
есть символы, которые нельзя декодировать в юникод. Каждая строка возвращённая итератором, обновляет позицию внутри объекта. Значит, мы можем читать произвольное количество строк, а не только до конца файла. В классе есть методы для сохранения позиции в кеш на диске и чтения её оттуда.
Модуль сам по себе является утилитой командной строки. Она выводит на консоль содержимое файла, сохраняя позицию его конца в кеше. При следующем запуске печатаются только строки, которые были записаны после предыдущего чтения. Выглядит это так:
$ echo -e "one\ntwo\nthree" > example
$ cutthelog example
one
two
three
$ cutthelog example
$ echo -e "four\nfive\nsix" >> example
$ cutthelog example
four
five
six
$ cutthelog example
$ echo -e "seven\neight\nnine\nten\neleven\ntwelve" > example
$ cutthelog example
seven
eight
nine
ten
eleven
twelve
Кеш хранится в файле .cutthelog
в домашней директории пользователя. Запуск под другим пользователем выведет файл полностью. Путь до кеша можно задать вручную опцией -c/--cache-file
.
Код модуля находится на github. Python пакет доступен на pypi. Надеюсь, они будут вам полезны.