Сепаратор для логов. Сжимаем логи для контекста LLM без потери читаемости

от автора

Я думаю, многим знакомо устройство под названием сепаратор-то, что отделяет сливки от молока. Моя библиотека logzip занимается примерно тем же самым — отделяет сливки больших логов, оставляя самую суть перед подачей их на анализ в LLM.

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

Началось все с того, что я здесь на Хабре прочитал статью https://habr.com/ru/articles/1026040/ камрада @sergeivsk и как раз в тот момент у меня была проблема анализа относительно больших логов при отладке кода. При относительно длинных дистанциях отладки мой внутренний экономист начинает жалеть токены, потраченные впустую на отсеивание в LLM постоянно повторяющихся строк, не несущих никакой смысловой нагрузки. Так и родилась идея создания logzip. Исходники @sergeivskя не смотрел, была позаимствована только идея. Как оказалось потом реализация в чем то совпала.


Итак, ситуация: у вас падает сервис, вы открываете лог и видите…. ~48k строк, а это примерно 10 МБ сырого текста, или 2-3 млн токенов для Claude:

типичный лог

INFO: 127.0.0.1:45678 - "GET /api/v1/status HTTP/1.1" 200 OK [12 ms]INFO: 127.0.0.1:45679 - "GET /api/v1/status HTTP/1.1" 200 OK [11 ms]INFO: 127.0.0.1:45680 - "GET /api/v1/status HTTP/1.1" 200 OK [13 ms]... (5000 одинаковых строк) ...ERROR: 127.0.0.1:51234 - "POST /api/v1/submit HTTP/1.1" 500 timeout [5000 ms]... (ещё 5000 успешных) ...

Первая проблема: Модель видит 5000+ успешных запросов и теряет одну критичную ошибку посередине. Контекст модели размазывается. Это известный эффект LLM — Lost in the Middle, когда информация в центре обрабатывается хуже чем в начале или в конце. Модель буквально тонет в сотнях однообразных строк.

Вторая проблема, исходящая из первой — вы платите за пустые строки не несущие никакой смысловой нагрузки. 90% лога — это однообразные INFO: 200 OK.


Некоторые скажут, «зачем еще один архиватор?», «есть grep! для таких вещей». И будут правы, но не во всем. Дело в том, что grep/gzip/zstd и logzip — это инструменты предназначенные для разных целей.

gzip < app.log | wc -c819 KB #Сжатие на 90%! Супер!

Попробуйте скормить этот результат в тот же Claude. Модель откажет — она не умеет читать бинарные данные. Нам нужно именно текстовое сжатие, которое:
— выглядит как текст;
— остается человекочитаемым (до определенной степени);
— самое важное: сохраняет смысл аномалий;
— экономит токены.

grep -i "error" # пробуем грепнуть вышеприведенный примерERROR: 127.0.0.1:51234 - "POST /api/v1/submit HTTP/1.1" 500 timeout [5000 ms]

«А почему не старый добрый grep?» спросят олды. Проблема в том что grep слишком радикален. Когда вы вырезаете из лога только строки с ошибками, вы лишаете модель контекста.

  • Как происходило развитие событий?

  • Что происходило за секунду, минуту до ошибки?

  • Какие запросы шли параллельно?

  • Был ли всплеск нагрузки?


Вместо того, что бы скрывать всё, (как gzip), или вырезать точно ошибку (как grep), я решил скрывать повторяющийся мусор. Тут реализация оказалась такое же как и подход @sergeivsk:

  • Найти все повторяющиеся вхождения типа INFO, GET /api/v1/status, 127.0.0.1

  • Заменить их на короткие токены #0#, #1#, #2#

  • Хранить маппинг в LEGEND

  • Оставить аномалии и уникальные строки в BODY в исходном виде

До обработки:

2026-04-21T14:32:00Z INFO uvicorn.access 127.0.0.1 - "GET /api/v1/users HTTP/1.1" 200 45ms2026-04-21T14:32:01Z INFO uvicorn.access 127.0.0.1 - "GET /api/v1/users HTTP/1.1" 200 52ms2026-04-21T14:32:02Z INFO uvicorn.access 127.0.0.1 - "POST /api/v1/orders HTTP/1.1" 201 123ms2026-04-21T14:32:03Z INFO uvicorn.access 127.0.0.1 - "GET /api/v1/status HTTP/1.1" 200 3ms... (100 строк успешных) ...2026-04-21T14:33:45Z ERROR uvicorn.error Database connection timeout after 30s

После обработки:

--- PREFIX ---2026-04-21T14:32 INFO uvicorn.access 127.0.0.1 ---- LEGEND ---#0# = GET /api/v1/users HTTP/1.1" 200#1# = GET /api/v1/orders HTTP/1.1" 201#2# = GET /api/v1/status HTTP/1.1" 200!1! = #0# 45ms       ← второй проход: комбинации тегов--- BODY ---:00Z #0# 45ms:01Z #0# 52ms:02Z #1# 123ms:03Z #2# 3ms... (короче) ...:45Z ERROR uvicorn.error Database connection timeout after 30s  ← аномалия видна!

Результат:

  • размер 8 Мб сократился до 3,4 Мб (~58%)

  • Читаемость 10/10 (модель понимает слёту)

  • Видимость ошибок: 10/10 (они не закрыты мусором)


Как это работает.

Мною был выбран Rust + PyO3, потому что это:
1. Скорость ~200x по сравнению с чистым Python. На огромных логах это критично. Так, те же 8 МБ обрабатывались на чисто пайтоновской реализации около 2 минут.
2. Безопасность. Нет unsafe блоков. Memory safety гарантирована.
3. PyO3: Rust код оборачивается в Python API и работает в pip install logzip

Алгоритм:

raw log   ↓[1] Profile Detection     ← определяем формат (journalctl/docker/uvicorn/pino)   ↓[2] Normalizer           ← убираем ANSI, наносекунды, leading zeros   ↓[3] Frequency Analysis   ← параллельный подсчёт n-грамм (rayon)   ↓[4] Legend Selection     ← жадный алгоритм с позиционным индексом (O(N), не O(N²))   ↓[5] AhoCorasick Replace  ← одноходная замена всех токенов   ↓[6] Recursive BPE        ← второй проход: сжимаем комбинации токенов   ↓compressed text

Почему это быстро?

Узкое место (было): в Python версии я считал working.count(value) в цикле — O(N²) алгоритм. На 8 Мб это две минуты.
Решение: Построить позиционный индекс один раз O(N)), потом жадно выбирать кандидаты с мемоизацией блокировки. Итого O(N log N).
Результат: 2 минуты сократились до 0,4 секунд. Ускорение в 215 раз.

Второй проход -Recursive BPE

После первого сжатия текст выглядит так:

#0# #1# 200 45ms#0# #1# 200 52ms#0# #1# 200 48ms

Видно что последовательность #0# #1# 200 повторяется. Второй проход сжимает ее в !1!:

!1! 45ms!1! 52ms!1! 48ms

Это действие дает еще 5-10% экономии за 18 мс доп. времени. BPE (Byte Pair Encoding) позволяет находить повторяющиеся цепочки уже созданных токенов, превращая последовательности вроде #0# #1# в новый супер-токен !1!»


После деплоя 1 версии в GitHub и на PyPI я увидел первые скачивания в статистике и задумался — а почем бы не прикрутить MCP? Что нам стоит дом MCP построить? Сказано — сделано!
Был написан MCP сервер и встроен в Claude и Cursor.

{  "mcpServers": {    "logzip": {      "command": "logzip",      "args": ["mcp", "--allow-dir", "/var/log"]    }  }}

MCP был успешно испытан на максимально доступных мне логах.

# Пользователь просто спрашивает:> Analyze /var/log/app.log# Claude автоматически:1. Вызывает get_stats /var/log/app.log   → Size: 15 MB (~3.7M tokens)   → After compression: ~6.3 MB (~1.5M tokens)   2. Вызывает compress_file /var/log/app.log --quality balanced --bpe-passes 23. Отправляет сжатый лог в контекст4. Начинает анализ

Бенчмарки и экономика

Benchmark на реальном ~8МБ логе (Uvicorn + Docker)

Режим

Время (мс)

Размер (КБ)

Сжатие

Комментарий

fast

200

4.900

~40%

Срочный анализ

balanced

404

3.928

~52%

Базовый выбор

balanced+BPEх2

418

3.404

~58%

Оптимум

max

507

3.511

~57%

Максимальное

Объяснение подвоха max: почему —quality max работает как —quality balanced?
Потому что:
1. После первого прохода с 512 entries мы уже раздавили 57% объема.
2. Второй проход работает БЕЗ того же материала.
3. Добавление 400 экстра записей в легенду- это просто раздуть вывод.
4. А bpe-passes делает второй ПРОХОД, который находит повторы в УЖЕ сжатом тексте. Зачем он нужен? Затем что второй проход ищет не новые «крупные» паттерны, а КОМБИНАЦИИ уже найденных тегов. Это более эффективно, чем просто добавить 400 редкоиспользуемых записей в легенду.

--quality max:      512 entries, 1 pass   → 507ms, -57%--quality balanced: 99 entries, 1 pass    → 404ms, -52%--quality balanced --bpe-passes 2:        → 418ms, -58% ← ПОБЕДИТЕЛЬ

Вывод: —quality max — переплата за медлительность при поиске повторов.

Экономика

┌──────────────────────────────────────────┐│ Сценарий: 10 анализов в день             ││ по 7.96 МБ логов каждый                  │├──────────────────────────────────────────┤│                                          ││ БЕЗ logzip:                              ││ • Размер: 8 МБ = ~1,960,000 токенов      ││ • На запрос: ~$2.00                      ││ • 10 запросов: $20/день = $600/месяц     ││                                          ││ С logzip (balanced --bpe-passes 2):      ││ • Размер: 3.4 МБ = ~830,000 токенов      ││ • На запрос: ~$0.85                      ││ • 10 запросов: $8.50/день = $255/месяц   ││                                          ││ Экономия: $345/месяц                     ││ Инвестиция: 10 минут на интеграцию       ││ ROI: 2070% в месяц                       │└──────────────────────────────────────────┘

Сырой лог:

... (3449 успешных запросов) ...INFO: 127.0.0.1:45678 - "GET /api/v1/status HTTP/1.1" 200 OKINFO: 127.0.0.1:45679 - "GET /api/v1/status HTTP/1.1" 200 OKERROR: Database connection timeout (пропущена в шуме!)INFO: 127.0.0.1:45681 - "GET /api/v1/status HTTP/1.1" 200 OK... (ещё 1500 успешных) ...

после logzip:

--- LEGEND ---#0# = INFO: 127.0.0.1:... - "GET /api/v1/status HTTP/1.1" 200 OK--- BODY ---#0##0##0#ERROR: Database connection timeout ← Кричит на всю страницу!#0##0#...

Модель сразу видит ошибку не утонув в 5000 одинаковых 200 ОК.

Это позволяет экономить реальные деньги.
Было (пример взят с «потолка»): $20/месяц на анализ логов
Стало: 8.5$/месяц


Как использовать

Установка

pip install logzip

CLI

logzip compress —quality balanced —bpe-passes 2 < app.log | pbcopy

Python API

from logzip import compressresult = compress(open("app.log").read(), bpe_passes=2)print(result.render(with_preamble=True))  # → в Claudeprint(result.stats_str())                  # → метрики

MCP

1. Установить бинарник

cargo install logzip

2.Добавить в ~/Library/Application Support/Claude/claude_desktop_config.json

3.Перезапустить Claude Code


Ссылки. Планы. Благодарности.
MIT лицензия.

Проект доступен на Github

Благодарю @sergeivsk за вдохновение

ссылка на оригинал статьи https://habr.com/ru/articles/1030964/