(Или как я написал радикально простую альтернативу Graylog)

В 2022 году я и моя команда работали над сервисом, который выводил довольно большой объём логов с распределённого кластера из 20+ хостов — всего около 2–3 миллионов сообщений в час. Мы тогда использовали Graylog, и запрос логов за последний час выполнялся за 1–3 секунды — довольно быстро.

Однако инженеры по инфраструктуре хотели избавиться от Graylog — он требовал от них утомительного обслуживания, и в итоге было принято решение перейти на Splunk. Когда Splunk наконец внедрили, мне пришлось с удивлением обнаружить, что он работает невероятно, возмутительно медленно. Честно говоря, глядя на это, я не совсем понимаю, как им вообще удаётся его продавать. Если вы работали со Splunk, то знаете, что там есть два режима: «Smart» и «Fast». В «умном» режиме тот же запрос логов за час занимал несколько минут. А в так называемом «быстром» — от 30 до 60 секунд (причём этот «быстрый» режим имеет и другие ограничения, которые делают его гораздо менее полезным). Возможно, это была какая-то неправильная настройка (я не специалист по инфраструктуре, так что точно не знаю), но никто не знал, как это исправить, да и не хотел особо разбираться. Стало ясно: как только Graylog окончательно отключат, мы потеряем возможность быстро просматривать логи, и это было для нас проблемой.

Мне это показалось абсурдным. 2–3 миллиона логов в час — это не так уж и много, и мне казалось, что с помощью старых добрых GNU утилит и обычных лог-файлов, без какого-либо централизованного сервера логирования, можно добиться примерно такой же скорости, как у Graylog (а по крайней мере — гораздо быстрее, чем у Splunk), и этого было бы достаточно для большинства наших задач. Мы тогда не использовали контейнеризацию: у нас были обычные AWS-инстансы с Ubuntu, на которых напрямую работал наш бэкенд в виде systemd-сервисов, выводящих логи в /var/log/syslog, то есть лог-файлы уже были у нас под рукой.

Так и начался этот проект: я не мог перестать об этом думать, взял отпуск на неделю и устроил себе личный хакатон, чтобы написать прототип: просмотрщик логов с простым но удобным TUI интерфейсом, включающим таймлайн-гистограмму. Он подключался к хостам по SSH и анализировал обычные лог-файлы с помощью GNU-утилит bash + tail + head + awk.

Вот что я показал коллегам на следующей неделе:

Nerdlog UI
Nerdlog UI

В конце есть gif-демо, а также ссылки на репозиторий GitHub и документацию; но если интересно, как это работает, читайте дальше.

Цели

Главная цель формулируется очень просто: реализовать утилиту для получения и просмотра логов, которая покрывает большую часть наших потребностей, ранее закрываемых Graylog. По моим прикидкам, в 95%+ случаев нам нужно было просто получить логи за последний час или день, отфильтрованные в соответствии с запросом, и также получить гистограмму активности по времени. И нужно было, чтобы это работало быстро. Всё.

Оставшиеся 5% или меньше — это более продвинутые функции, вроде подсчёта статистики по сообщениям: чаще всего просто сколько раз встретилось то или иное сообщение, чтобы найти самые частые сообщения.

Очевидно, я сосредоточился на 95%. Так что более конкретные цели были такими:

  • Получать логи с нескольких удалённых хостов одновременно

  • Фильтрация логов по диапазону времени и по шаблонам

  • Гистограмма по времени (timeline histogram) для отображения активности за выбранный период

  • В UI отображаются только самые последние логи из выбранного периода, с возможностью догрузки старых постранично

  • Запрос логов должен быть достаточно быстрым. Моя идеальная цель — добиться уровня производительности, как у Graylog (1–3 секунды на обработку 2–3 миллионов логов за час), но это не было жёстким требованием. Главное — быть заметно быстрее, чем Splunk, и этого уже достаточно. Но если честно, мне очень хотелось дотянуться до скорости Graylog.

На этом с целями всё. Дальше — про реализацию.

Реализация: знакомьтесь, Nerdlog

Общая идея была такая:

  • Клиент — это терминальное TUI-приложение, которое отображает логи и гистограмму активности по времени. Написано на Go с использованием отличной библиотеки для терминальных интерфейсов — tview;

  • Приложение-клиент устанавливает отдельное SSH-соединение к каждому хосту, с которого нужно получить логи;

  • Чтобы отфильтровать нужные логи и построить данные для гистограммы, на удалённой машине используются обычные GNU-утилиты вроде bash, awk и т.д. — прямо по SSH. Это значит, что не нужно ничего ставить на сервера, и всё будет работать сразу на любом стандартном дистрибутиве Linux или FreeBSD;

  • На клиенте мы агрегируем полученные данные (логи и данные для гистограммы) со всех хостов, и отображаем их в интерфейсе.

Так как терминальный интерфейс сам по себе немного «нердский», а Graylog (пусть и отдалённо) был одним из источников вдохновения, я назвал проект Nerdlog.

Самая важная и спорная часть реализации — это быстрая фильтрация логов на стороне хоста с помощью стандартных GNU утилит. Как я уже упоминал в разделе про цели, моя «полярная звезда» — добиться такой же скорости, как у Graylog. На это ушло несколько итераций, прежде чем всё заработало как надо. Так что давайте поговорим об этом подробнее.

Скрипт-агент

Скрипт-агент — это та часть, которая выполняется на удалённых хостах и отвечает за фактическую фильтрацию логов. Это просто bash-скрипт, использующий стандартные GNU-утилиты.

При подключении к каждому хосту, Nerdlog создаёт этот скрипт во временной папке /tmp, а затем запускает его по мере необходимости.

Прежде всего, давайте определимся: откуда именно мы будем читать логи?

Источники логов

Есть два основных варианта, откуда читать логи: либо напрямую из обычных лог-файлов вроде /var/log/syslog/var/log/syslog.1 и т. д.), либо через journalctl.

Сразу скажу: в 2025 году Nerdlog уже поддерживает оба варианта, так что вы можете использовать тот, который вам удобнее. Но в 2022-м, когда я только начинал и писал прототип в режиме хакатона, мне нужно было выбрать что-то одно. Поэтому давайте немного разберёмся:

Плюсы journalctl:

  • Для задачи Nerdlog с ним может быть проще работать: чтобы получить логи за нужный промежуток времени, достаточно вызвать команду вроде journalctl --since '2022-05-08 01:00:00' --until '2022-05-09 08:00:00' — и всё готово; не нужно вручную искать нужные файлы по дате.

  • Универсально доступен на большинстве Linux-дистрибутивов (в 2025 году даже чаще, чем обычные лог-файлы: некоторые дистрибутивы по умолчанию даже не ставят rsyslog, что грустно, но факт).

  • Хранит логи дольше, чем стандартные лог-файлы (чаще всего).

Минусы journalctl:

  • Он значительно медленнее, чем просто чтение файлов. И я имею в виду, значительно — в моем конкретном тестовом сценарии, journalctl оказался примерно в 90 раз медленнее. Подробнее об этом — вот тут на GitHub.

  • Он может пропускать логи (впрочем, справедливости ради, Graylog тоже может). Мы замечали, что во время пиков активности какие-то сообщения просто не появляются в выводе journalctl, хотя в обычных лог-файлах они есть. Я читал, что происходит что-то вроде ограничения по частоте / переполнения буфера, и часть логов теряется. А при этом файловый лог продолжает их спокойно записывать. В общем, старые добрые текстовые лог-файлы оказываются надёжнее, чем новые модные штуки.

  • Он отсутствует на FreeBSD и других не-Linux системах (в нашем изначальном случае это было неважно, но упомяну для полноты картины).

Уже из-за первого минуса выбор был очевиден: journalctl не подходил, потому что производительность была для меня ключевым приоритетом, а обычные файлы работали намного быстрее.

Ну и, конечно, я не хотел иметь дело с "пропавшими логами", когда у меня есть способ этого избежать.

Фильтрация логов

Итак, решено: читаем логи из обычных файлов. Теперь давайте поговорим о том, как их обрабатывать.

Нам нужно реализовать два уровня фильтрации:

  1. По диапазону времени — чтобы просматривать только интересующий нас период;

  2. По шаблонам (паттернам) — чтобы оставить только нужные строки.

Я довольно быстро остановился на использовании awk в качестве основного инструмента для обработки данных: он установлен по умолчанию почти везде (чаще, чем, скажем, perl), очень быстрый, и в целом, было ощущение, что это идеальный инструмент для решаемой задачи.

Фильтрация по шаблонам

С помощью awk фильтрация по шаблонам — это самая простая часть. У awk есть встроенная поддержка логических операторов, которые могут быть комбинированы с регулярками, так что, например, чтобы найти все строки, не содержащие foo, но содержащие bar или baz, можно просто написать:

!/foo/ && (/bar/ || /baz/)

Это очень классно — мы получаем почти такую же гибкость запросов, как в Graylog, но без необходимости писать собственный парсер запросов или использовать нечитаемые регулярки.

Так что, если пока забыть про фильтрацию по времени, можно написать вот такой простой скрипт на bash+awk, который просто выводит подходящие строки из двух последних лог-файлов:

#!/usr/bin/env bash
 
pattern="$1"
if [[ "$pattern" == "" ]]; then pattern="//"; fi
 
awk_script='
  # Filter out lines not matching the pattern
  !('"$pattern"') { next }

  # For now, just print the matching lines
  { print $0 }
'
 
cat /var/log/syslog.1 /var/log/syslog | awk "$awk_script" -

Вызывать его можно, например, так:

bash myscript.sh '!/foo/ && (/bar/ || /baz/)'

Таким образом, фильтрация по шаблонам получилась на удивление простой и элегантной. Но вот фильтрация по диапазону времени потребовала больше работы — давайте теперь разберём и её.

Фильтрация по временному диапазону: первая наивная попытка

Вот как выглядит классический формат syslog (или "messages"):

Mar 10 10:20:17 myhost myprogram[4163]: Something happened

Важно то, что временная метка в начале имеет фиксированную длину; даже если день — это всего одна цифра (например, Mar 9), он дополняется пробелом:

Mar  9 23:58:01 myhost myprogram[4163]: Something happened

А значит, мы можем просто передавать границы временного диапазона как два аргумента в точно таком же формате, например Mar 10 11:00, и в скрипте awk добавить два условия, отфильтровывающих строки вне диапазона:

# Filter out lines outside of the time range
(substr($0, 1, 12) <  "Mar 10 11:00") { next }
(substr($0, 1, 12) >= "Mar 10 12:00") { next }

Это работает. Но проблема в том, что работает не так быстро, как хотелось бы. Для небольших логов — нормально, но наши логи были совсем не маленькие.

Немного подробнее об объеме логов: как я уже упоминал, у нас было более 20 хостов, и в сумме с них приходило 2–3 миллиона лог-сообщений в час. Предположим, что средняя строка лога занимает 200 байт (это не только само сообщение, но и контекстные поля вроде field1=value1, field2=value2 и т.д.).

Итак: 200 байт на строку × 3 миллиона строк в час × 24 часа, делим на 1 ГБ = примерно 13.4 ГБ логов в день.

Данные распределялись более-менее равномерно по всем 20 хостам; то есть, на один хост приходилось около 600–700 МБ логов в день.

При этом нужно обрабатывать два файла логов: /var/log/syslog.1 (за предыдущий полный день) и свежий /var/log/syslog (за текущий неполный день). В сумме это даёт от 600 до 1400 МБ данных, с которыми обычно нужно работать на одном хосте. Для удобства будем считать, что средний объем логов — это 1 ГБ.

И по моим грубым замерам, обработка такого 1-гигабайтного файла с помощью приведенного выше awk-скрипта занимала 2–3 секунды (и это без логики построения временной гистограммы, получения последних сообщений и отправки всего этого клиентскому приложению Nerdlog — что увеличило бы общее время еще как минимум в 2–3 раза).

Если честно, даже 5–10 секунд — это уже совсем неплохо по сравнению с несколькими минутами, которые у нас уходили на то же самое в Splunk. Но я был уверен, что можно сделать ещё лучше. Ведь чаще всего нас интересует лишь крошечная часть всех доступных логов, а значит, большая часть времени awk просто тратится впустую (и так — при каждом новом запросе). Впрочем, то же самое можно сказать и про фильтрацию по шаблонам, но там улучшить особо нечего, а вот с временными диапазонами — можно.

Фильтрация по временному диапазону: индекс по номерам строк

Удобство работы с временными диапазонами заключается в том, что строки логов отсортированы по времени (это в большинстве случаев: есть исключения, но пока не будем на них отвлекаться). То есть, "фильтрация по временному диапазону" на деле сводится к выбору одного непрерывного куска из всех строк логов. Или, точнее, к отсечению неактуальных строк в начале и/или конце.

В идеале, мне хотелось бы иметь инструмент, который умеет выполнять бинарный поиск по строкам в отсортированных текстовых файлах и возвращать номера строк (или, ещё лучше, смещения в байтах — но об этом поговорим ниже). То есть я бы мог спросить его: «Найди первую строку, которая начинается с Mar 10 06:00», — и получить номер строки в ответ. Зная его, я бы мог с помощью tail и head быстро отсечь всё лишнее вне диапазона и передать оставшееся в awk.

К сожалению, я не смог найти стандартную GNU утилиту, которая бы это умела. Досадно. Поэтому я пришёл к выводу, что нужно строить индекс: просто маппинг из временной метки вроде Mar 10 06:00 в соответствующий номер строки. В простейшем виде скрипт на awk для генерации такого индекса может выглядеть так:

BEGIN { last_time = "" }
{
  cur_time = substr($0, 1, 12);
  if (cur_time > last_time) {
    last_time = cur_time
    print cur_time "\t" NR
  }
}

Результат будет таким:

Mar 10 00:47	1
Mar 10 00:48	2431
Mar 10 00:49	5291
Mar 10 00:50	8196
Mar 10 00:51	11096
...

Этот скрипт тоже выполняется за 2–3 секунды на том же 1GB лог-файле, но при этом его нужно запускать только один раз после ротации логов (обычно это раз в день, а иногда и раз в неделю — зависит от конфигурации rsyslog). В остальное время достаточно индексировать только "вверх" по необходимости: если пользователь запрашивает метку времени позже последней в индексе — мы просто доиндексируем оставшуюся часть лог-файла, что значительно быстрее, чем начинать с нуля.

Имея такой индекс, мы можем изменить исходный bash-скрипт примерно так (для простоты демонстрации здесь опущены всевозможные граничные случаи, и мы работаем только с одним лог-файлом):

#!/usr/bin/env bash

LOGFILE=/var/log/syslog
INDEXFILE=/tmp/nerdlog_index

pattern="$1"
if [[ "$pattern" == "" ]]; then pattern="//"; fi

from="Mar 15 07:00" # Just hardcode timestamps for demo purposes
to="Mar 15 08:00"

# Takes a timestamp like "Mar 10 00:54", and prints the first line number
# with that or later timestamp.
function get_lineno_from_index() {
  awk -F"\t" '($1 >= "'"$1"'") { print $2; exit; }' "$INDEXFILE"
}

from_lineno="$(get_lineno_from_index "$from")"
to_lineno="$(get_lineno_from_index "$to")"

awk_script='
  # Filter out lines not matching the pattern
  !('"$pattern"') { next }

  # For now, just print the matching lines
  { print $0 }
'

tail -n +$from_lineno "$LOGFILE" |         \
  head -n $((to_lineno - from_lineno)) |   \
  awk "$awk_script" -

С этим улучшением прирост производительности оказывается значительным: на том же 1GB лог-файле, если нас интересует только один час из последних суток, то вместо 2–3 секунд обработка теперь занимает 300–400 мс, то есть примерно в 7–8 раз быстрее.

И теперь awk-скрипт вообще не заботится о временных диапазонах — вся фильтрация по времени целиком перенесена на tail и head, а awk получает на вход только релевантные данные.

Фильтрация по временному диапазону: индекс по смещению в байтах

Но можно сделать ещё лучше: tail -n и head -n работают с номерами строк, поэтому им приходится последовательно сканировать все строки до нужной, и это замедляет процесс.

Поскольку мы уже генерируем индекс-файл, то вместо хранения номеров строк можно сохранять байтовые смещения. Тогда мы сможем использовать tail -c и head -c, которые работают с байтами. Причём tail -c (который читает файл с диска) на самом деле под капотом просто делает lseek, что работает практически моментально вне зависимости от смещения.

Чтобы создать индекс с байтовыми смещениями, нужно запускать awk с флагом --characters-as-bytes (или просто -b), потому что иначе awk считает многобайтовые символы длиной 1, и невозможно получить корректное байтовое смещение.

К сожалению, всё это — awk -b, tail -c, head -c — расширения GNU и не совместимы с POSIX. Но для тех случаев, на которые ориентирован Nerdlog, вполне разумно рассчитывать на наличие GNU и получить дополнительную производительность. Если же реально будут случаи, где важна POSIX-совместимость — можно реализовать и fallback-режим. Дайте знать, если это кому-то нужно.

Итак, модифицированный awk-скрипт для генерации индекса выглядит так:

# NOTE: awk must be called with --characters-as-bytes or -b flag
BEGIN { last_time = ""; bytenr = 1; }
{
  cur_time = substr($0, 1, 12);
  if (cur_time > last_time) {
    last_time = cur_time
    print cur_time "\t" NR "\t" bytenr
  }

  bytenr += length($0)+1;
}

Результат будет:

Mar 10 00:47	1	1
Mar 10 00:48	2431	486204
Mar 10 00:49	5291	1010581
Mar 10 00:50	8196	1827708
Mar 10 00:51	11096	2418928
...

(Номера строк мы всё ещё сохраняем — они полезны, например, для отображения оригинального номера строки сообщения в UI)

Полный обновлённый bash-скрипт я приводить не буду, так как изменения нужно внести минимальные: вместо $2 в индексе использовать $3, заменить -n на -c для tail и head, и переименовать переменные, чтобы они обозначали байтовые смещения, а не номера строк.

Это даёт дополнительное ускорение в 2–3 раза (то есть в сумме 15–20-кратное ускорение по сравнению с наивной реализацией) на том же 1GB лог-файле — теперь обработка занимает 100–150 мс.

Вот теперь уже действительно интересно! Хотя это пока только фильтрация, без вывода полезных данных, но по скорости уже становится очевидно, что подход рабочий.

Вывод полезных данных

Теперь, когда у нас реализована вся необходимая фильтрация, нужно сделать что-то полезное с полученными логами. Как упоминалось ранее, приложению Nerdlog нужно два основных типа данных от каждого хоста:

  1. Данные для построения гистограммы по времени (для всего выбранного периода);

  2. Несколько последних сообщений логов, чтобы показать их в UI.

Для построения гистограммы нам просто нужно сопоставить определённые временные периоды (например, по одной минуте) с количеством сообщений, пришедших в этот период. В Nerdlog используется фиксированная дискретизация в 1 минуту, так что в качестве ключа можно использовать буквальное значение минуты из строки лога: "Mar 10 09:39".

А чтобы вывести последние N сообщений, мы просто будем поддерживать кольцевой буфер этих сообщений — ещё один маппинг, где ключи это числа от 0 до N.

В конце мы просто все это печатаем. Вот итоговый awk-скрипт:

awk_script='
  BEGIN { curline=0; maxlines=10; }
 
  # Filter out lines not matching the pattern
  !('"$pattern"') { next }
 
  {
    # Add the current message to the timeline histogram data.
    stats[substr($0, 1, 12)]++;
 
    # Maintain the circular buffer of the last log messages.
    lastlines[curline] = $0;
    curline++
    if (curline >= maxlines) {
      curline = 0;
    }
  }
 
  END {
    # Print the histogram data.
    for (x in stats) {
      print "s:" x "," stats[x]
    }
 
    # Print the most recent log messages.
    for (i = 0; i < maxlines; i++) {
      ln = curline + i;
      if (ln >= maxlines) {
        ln -= maxlines;
      }
 
      if (!lastlines[ln]) {
        continue;
      }
 
      print "m:" lastlines[ln];
    }
  }
'

И пример вывода:

s:Mar 10 00:52,1892
s:Mar 10 00:53,3044
s:Mar 10 00:54,1201
s:Mar 10 00:55,1459
s:Mar 10 00:56,1294
s:Mar 10 00:57,940
s:Mar 10 00:58,1356
s:Mar 10 00:59,1320
m:Mar 10 00:59:54 myhost kern[5156]: <err> Authentication failure
m:Mar 10 00:59:54 myhost daemon[1069]: <alert> SSH connection established
m:Mar 10 00:59:54 myhost news[1940]: <emerg> Security alert raised
m:Mar 10 00:59:54 myhost uucp[4680]: <debug> Permission denied
m:Mar 10 00:59:54 myhost user[5850]: <err> Memory leak detected
m:Mar 10 00:59:54 myhost user[6036]: <notice> Database migration failed
m:Mar 10 00:59:55 myhost ftp[2145]: <warning> Service initialization failed
m:Mar 10 00:59:55 myhost cron[972]: <alert> Scheduled task failed
m:Mar 10 00:59:55 myhost auth[3112]: <emerg> System rebooted
m:Mar 10 00:59:56 myhost mail[5664]: <debug> Network unreachable

Это добавило ещё 2–3 раза к времени выполнения, так что мы снова вернулись к 200–300 мс — что всё равно более чем достаточно.

И это, по сути, минимально необходимый агент-скрипт. Клиентское приложение читает эти строки из stdout, парсит их, агрегирует данные со всех хостов и показывает пользователю.

Да, тут пока не хватает многих деталей, чтобы всё работало корректно во всех случаях, но в целом — это рабочий прототип агентского скрипта для выборки логов из обычных лог-файлов. И, по моему мнению, он впечатляюще быстрый.

Пропущенные детали

Я, конечно, опустил здесь кучу вещей — таких как обработка ошибок, чтение логов из нескольких файлов, инвалидизация и регенерация индекса, сжатие данных при передаче, вывод прогресса, обработка граничных случаев (например, если запрошенные временные метки находятся за пределами доступных логов), логи с убыванием временных меток (такое бывает изредка), разные форматы таймстампов и так далее, и так далее.

Так что реальная реализация не будет настолько простой. На самом деле, сейчас этот агент-скрипт такой запутанный и плохо читаемый, что я даже не стану выкладывать ссылку на него здесь. Я надеюсь когда-нибудь переписать его основательно, но пока не заморачиваюсь — он работает и покрыт хорошими тестами, так что я не особо боюсь что-то сломать. Если вы очень хотите его увидеть — найдите файл nerdlog_agent.sh в репозитории сами (ссылку на репу я дам ниже). Оставь надежду всяк туда входящий.

Клиентское приложение

Когда стало понятно, что вытягивать логи из обычных текстовых файлов — это вполне рабочий подход, настало время заняться клиентским приложением Nerdlog. Оно состоит из двух основных частей, и обе были относительно просты в реализации:

  • UI: Наверное, больше всего времени ушло на то, чтобы заставить работать гистограмму по времени так, как мне хотелось — пришлось писать отдельный виджет с нуля и вручную обрабатывать всё рисование и события. Всё остальное, по сути, из коробки поддерживается tview (огромное спасибо автору!)

  • Управление соединениями и агрегация логов: это заняло чуть больше времени, но тоже ничего особенно интересного.

В подробности реализации я здесь вдаваться не буду — если интересно, загляните в репозиторий на GitHub.

Результаты хакатона

Когда всё было собрано вместе, производительность запросов оказалась даже лучше, чем я надеялся: тот же запрос, обрабатывающий 2–3 миллиона логов за последний час, почти всегда укладывался в 2 секунды, что было даже лучше Graylog в нашем сценарии. И дело не только в скорости — решение оказалось ещё и надёжнее: в отличие от Graylog, оно не теряло и не перемешивало сообщения.

Мне вообще кажется интересным и приятным тот факт, что решение, которое концептуально очень простое, может быть настолько эффективным и надёжным. Это же просто текстовые файлы, обрабатываемые стандартными инструментами, которые существуют уже десятки лет, через обычный ssh. Ничего крутого. Я, если честно, скучаю по такой простоте в современном софте — сейчас всё как будто намеренно перегружено и переусложнено.

Демо

Наконец, вот небольшой gif, демонстрирующий Nerdlog в действии, где он работает с четырьмя удаленными хостами:

Nerdlog working with 4 remote hosts
Nerdlog working with 4 remote hosts

Статус проекта

С момента первой реализации в 2022 году в коде всё ещё остаются следы «хакатонного» стиля, и его можно было бы отполировать, но в целом проект заметно повзрослел. Кратко подытожу:

  • С хостов скачивается только минимально необходимое количество данных — это экономит и время, и трафик;

  • Большинство данных передаётся в сжатом виде (gzip), что экономит трафик еще больше;

  • Помимо обычных лог-файлов, теперь также поддерживается journalctl (работает медленнее, чем чтение обычных логов, но это заметно только если логов действительно много);

  • Кросс-платформенность: протестировано на разных дистрибутивах Linux, FreeBSD, macOS и Windows (на Windows работает только клиентское приложение — получать логи с Windows-хостов нельзя).

Я использую Nerdlog уже несколько лет — и на работе, и в личных проектах — и считаю, что он готов к использованию на реальных production-системах. Конечно, ещё можно многое улучшить, и есть ряд функций, которые я бы хотел добавить (см. ниже), но уже сейчас он решает реальные задачи с помощью очень простых методов.

Так как же Nerdlog сравнивается с Graylog?

Очевидно, что это не полноценный аналог Graylog, но лично для меня Nerdlog оказался вполне жизнеспособной альтернативой. Для большинства моих задач по логированию его более чем достаточно. А в некоторых аспектах он даже лучше:

  • Он не теряет и не перемешивает логи, как это иногда делает Graylog. По крайней мере, в нашей конфигурации Graylog был настроен через UDP, а это значит, что сообщения могут теряться (и действительно терялись), и если несколько сообщений приходят с одинаковым таймстампом до миллисекунды — они могут отображаться в неправильном порядке, что раздражает.

  • Это субъективно, но лично мне приятно пользоваться шустрым терминальным TUI-приложением с клавиатурной навигацией, вместо немного неуклюжего web-интерфейса. У вас может быть другое мнение, конечно.

Кроме того, тот факт, что не требуется настройка или централизованный сервер, особенно интересен: это не только делает Nerdlog очень подходящим для небольших хобби-проектов и стартапов, но и открывает интересные варианты использования для DevOps/системных администраторов, которым часто нужно работать с произвольными серверами, настроенными разными людьми. С большинством этих систем Nerdlog будет работать из коробки.

Тем не менее, у Graylog тоже есть свои сильные стороны, разумеется:

  • Для контейнеризованных окружений Graylog, скорее всего, подходит лучше. Теоретически можно настроить Nerdlog и в Kubernetes / Nomad, но, возможно, это уже будет идти вразрез с его философией.

  • У Graylog есть некоторые полезные фичи, которых в Nerdlog пока нет:

Отсутствующие, но важные фичи

Парсинг пользовательских полей

Как и Graylog, и другие системы логирования, Nerdlog на самом деле поддерживает контекстные поля для каждого лог-сообщения — как видно из демо выше, UI отображает не просто сырые строки, а разбивает их как минимум на время и само сообщение, плюс дополнительные поля из формата syslog: это program, hostname и pid. Есть также поле lstream, но оно особенное — оно не из логов, а задаётся по имени лог-потока (“logstream”), из которого эти логи были собраны (подробности в документации).

Проблема в том, что сейчас это всё нельзя кастомизировать: логика парсинга syslog захардкожена в Go (можно посмотреть функции parseLogMsgEnvelopeDefault и parseLogMsgLevelDefault в файле lstream_client.go), и на данный момент нет способа задать парсинг сообщений для конкретного приложения, не правя исходники Go.

Собственно, именно так мы и делали у себя: добавляли в конец строки поля в формате field1=value1 field2=value2 и писали свой кастомный Go-функционал, чтобы их распарсить.

Вы можете сделать так же (проект ведь с открытым кодом), но чтобы упростить жизнь, я планирую внедрить поддержку скриптов на Lua — чтобы пользователь мог описывать свою логику парсинга сообщений в любом формате. Это позволит иметь произвольное количество пользовательских полей без необходимости трогать Go-код.

Кстати, чтобы избежать недопонимания: парсинг сообщений (будь то на Go или Lua) происходит только в клиентском приложении — то есть только для сообщений, которые будут показаны в UI. То есть, это вообще не замедляет фильтрование логов, описанное выше, поскольку оно никак не зависит от парсинга. Только когда клиент получил логи от хостов, тогда уже он парсит сообщения перед их отображением.

Статистика

Ещё одна отсутствующая, но полезная фича — это статистика: например, получение списка уникальных сообщений и количества их повторений.

В Graylog это называется Quick Values. В Nerdlog мы не можем реализовать это точно так же, как в Graylog, потому что Quick Values там завязаны на поля и их значения. А в Nerdlog — даже несмотря на то, что отдельные поля у нас тоже есть (см. предыдущий раздел) — эти поля существуют только на стороне клиента. В awk-скрипте, который занимается выборкой данных, их нет, а значит и универсального способа строить по ним статистику тоже нет.

Что мы можем сделать — это использовать регулярные выражения с группами захвата. Например, если в логах есть что-то вроде myfield=some_value, мы могли бы писать регулярку типа \bmyfield=([a-zA-Z0-9_]*) и получать статистику по всем совпадениям и количеству их появлений.

Такая потребность возникает не очень часто, поэтому эта фича до сих пор не реализована, но я всё ещё планирую это сделать когда-нибудь. Для меня это буквально последняя крупная фича, которой иногда не хватает в Nerdlog по сравнению с Graylog.

Код и документация

(Эта статья - перевод моей статьи на английском. Т.к. автор оригинальной статьи - я сам, то с вашего позволения, я не стал помечать эту статью как перевод)

Комментарии (7)


  1. SlavikF
    23.05.2025 15:08

    Довольно "хакерский" проект.

    На Github перечислены некоторые ограничения, - например то, что работа Nerdlog нагружает серверы на которых запущены сервисы.

    Я бы добавил ещё одно ограничение: если сервер взломали, то с Nerdlog я логи посмотреть не смогу. А вот если логи собирались на Elastic / Splunk / ... - то логи того, как лезут на сервер у меня останутся.


    1. dimonomid Автор
      23.05.2025 15:08

      Вы правы, но это следствие того же ограничения. На Github в этом пункте о дополнительной нагрузке на сервер также указано, что если сервер вообще не отзывается по любой причине, то и логи посмотреть не получится. Но наверное вы правы, что стоит упомянуть и про сценарий со вломом в этом же пункте (если взломщики были настолько аккуратны, что стерли свои следы из логов).


      1. SlavikF
        23.05.2025 15:08

        Это не столько о том, что стёрли логи, а ещё про сценарий когда потеряли доступ к серверу. Заблокировали пользователя.

        Или когда диск сдох. То в отправленных логах ещё бывает можно что-то увидеть, а вот на сам сервер зайти бывает уже не получается.


        1. dimonomid Автор
          23.05.2025 15:08

          А, ну тогда это уже указано в доках (выделение мое):

          Likewise, if the host becomes unresponsive for whatever reason, we can't get logs from it either.

          И следующий параграф там также упоминает, что это можно решить синхронизацией логов на отдельный хост; rsyslog это умеет, насколько мне известно (но сам я не заморачивался. KISS и все такое).


  1. gudvinr
    23.05.2025 15:08

    Хаб $mol как будто бы лишний тут


    1. dimonomid Автор
      23.05.2025 15:08

      Хмм, я не завсегдатай на Хабре, и, похоже, неправильно понял предназначение этого хаба: был почему-то уверен, что он для любых небольших утилит и т.д., но сейчас вижу, что это про конкретный фреймворк. Извиняюсь! Нужно было осмотреться получше.

      Убрал его из статьи, спасибо, что указали!

      Пока что просто в open source, значит. Если считаете, что статья хорошо подойдет в какие-то другие хабы, буду рад совету. Я еще думал про DevOps (т.к. знаю пару devops инженеров, использующих Nerdlog), но т.к. статья скорее про детали реализации, то сомневался про этот хаб, и пока не стал туда добавлять.


  1. Kahelman
    23.05.2025 15:08

    Поздравляю вы почти изборели либо «шитые файлы» - текстовые файлы когда первый столбец содержит длину строки - тогда не приходится просматривать все символы в поисках перевода строки.

    Либо недоизобрели базу данных.

    В вашем первом примере вызов

    Cat filename | awk … можно упростить. Выкинуть cat. Awk с флагом -f позволяет читать сразу из файла.

    Если вы обрабатываете файл - создаете индекс по датам, то почему бы не сохранить контент в SQLite?

    Тогда у вас будет нормальный поиск/индекс по датам и с помощью full text search расширения SQLite сможете искать все что угодно и когда угодно.