Минимальные требования к самописному логгеру и чек-лист интеграционных тестов
· Го Комура · Разработка Windows, Логирование, Интеграционные тесты, Проектирование тестов, Надёжность
Если есть возможность использовать готовый logging framework, это более безопасный выбор. И всё же бывают ситуации, когда ограничения приложения или условия эксплуатации делают самописный logger неизбежным. Первое, над чем обычно ломают голову, — сколько именно реализовать, чтобы получить проектирование «не слишком небрежное, но и не слишком тяжёлое».
В этой статье мы сужаем предмет до логов приложения, используемых для расследования сбоев. Вместо того чтобы сразу взваливать на себя аудиторский след, распределённую трассировку, платформу метрик и облачную агрегацию, мы сначала определим минимальную конфигурацию, полезную на практике, а затем разберём интеграционные тесты, необходимые, чтобы эта конфигурация действительно заслуживала доверия.
Сначала вывод
Вот суть, которую стоит закрепить в первой версии.
- Формат —
UTF-8JSON Lines - Никогда не нарушать правило «одна запись — одна строка»
- Обязательные поля:
timestamp,level,category,message, структурированныеfields,sessionId,processId - Базовый принцип —
один файл на процесс - При низкой нагрузке — синхронная запись, при более высокой —
single writer + ограниченная очередь Error/Criticalи записи начала/конца сессии сбрасывать (flush) синхронно- Ротацию и хранение включать с версии v1
- Если место назначения логов недоступно, не переключаться молча на другое место
Если сузить требования примерно до этого уровня, и реализация, и эксплуатация становятся заметно устойчивее к сбоям.
Сначала сужаем область применения
Самописные логгеры часто усложняются потому, что с самого начала пытаются охватить всё. Стоит попытаться объединить в одном механизме диагностические логи, аудиторские логи, измерение производительности, распределённую трассировку и анализ поведения пользователей — и требования взрываются сразу.
Здесь предмет — диагностические логи, используемые для локализации сбоев приложения. То есть в приоритете возможность впоследствии проследить, «когда», «в какой операции», «что произошло» и «каким был контекст в этот момент». Одно только это сужение заметно облегчает первые проектные решения.
Минимально необходимые требования
1. Формат — UTF-8 JSON Lines
Логи можно хранить и в виде простой конкатенации текста, но впоследствии это плохо поддаётся машинной обработке. С другой стороны, если сразу выбрать тяжёлый собственный бинарный формат, это снижает наблюдаемость в эксплуатации.
Удобной золотой серединой служит UTF-8 JSON Lines. При одной записи на строку файл остаётся читаемым как текст и легко разбирается впоследствии скриптами и инструментами. Даже если запись обрывается на середине, легко понять, какая именно строка повреждена, — практическое преимущество.
2. Сразу зафиксировать обязательные поля
Минимальный набор полей, который стоит иметь, — вот эти семь.
timestamplevelcategorymessagefieldssessionIdprocessId
Лог, состоящий только из строки message, становится проблемой, когда впоследствии множатся критерии поиска. С другой стороны, слишком много полей резко увеличивает нагрузку на вызывающий код. Безопаснее всего зафиксировать набор примерно на этом уровне сначала и рассматривать добавления только тогда, когда они действительно понадобятся.
3. Базовый принцип — один файл на процесс
Проектирование, при котором несколько процессов дописывают в один и тот же файл, несёт больше рисков, чем кажется на первый взгляд. Взаимное исключение, частичная запись, момент ротации и обработка аварийного завершения сразу становятся сложными.
Начните с принципа один файл на процесс как базового. Если нужно объединить несколько процессов, безопаснее агрегировать данные на следующем этапе или явно поднять выделенный процесс агрегации.
4. Разделять стратегию записи по нагрузке
Пока объём логов невелик, синхронная запись понятнее и облегчает расследование сбоев. Принудительная асинхронность рискует потерять записи прямо перед завершением работы или оставить неопределёнными условия flush при исключениях.
С другой стороны, если объём логов растёт и синхронный I/O становится узким местом, стоит принять схему single writer + ограниченная очередь. Здесь важно заранее решить политику на случай переполнения. Не оставляйте неопределённым, отбрасывать ли старые логи, отбрасывать ли новые или выдавать предупреждение.
5. Определить условия flush
Синхронный flush записей Error и Critical, а также логов начала и конца сессии, окупается при расследовании сбоев. Если сбрасывать вообще всё, вплоть до обычных Info, это замедляет работу, поэтому не относиться ко всему одинаково — реалистичное решение.
6. Включить ротацию и хранение с версии v1
Ротацию часто считают тем, что «можно добавить позже», но это функция, отсутствие которой внезапно причиняет боль, как только начинается эксплуатация. Схема может быть любой — по размеру, посуточно, при каждом запуске, — но как минимум должно быть решено, что «файл не растёт бесконечно» и «сколько файлов хранится».
7. Не устраивать импровизированное резервное сохранение при сбое записи
Проектирование, при котором логгер молча пишет куда-то ещё, если место назначения логов недоступно, затрудняет последующее расследование. Сам факт того, что логов нет там, где они должны быть, задерживает первую реакцию эксплуатации на инцидент.
Если сохранить не удалось, проявите сбой через явно видимый канал: уведомление внутри приложения, журнал событий или стандартный поток ошибок. Как минимум стоит избегать состояния «никто не знает, куда делись логи».
Ориентировочная минимальная конфигурация v1
Для первой версии часто достаточно примерно вот этого.
UTF-8 JSON Lines- Один файл на процесс
- Имена файлов по сессиям
- Ротация по размеру или при каждом запуске
- Верхняя граница числа хранимых файлов
- Синхронный flush
Error/Critical - API, принимающий структурированные
fields
Всё сверх этого стоит добавлять только после того, как реальная эксплуатация покажет, «что действительно причиняло боль», — тогда логгер в итоге легче сопровождать.
Частые антипаттерны
Вот типичные вещи, которых стоит избегать.
- Затолкать всё в строку
message - Делить один и тот же файл между несколькими процессами
- Полностью перейти на асинхронность, не определив условия flush
- Откладывать ротацию и хранение
- Молча переключаться на другую папку при сбое сохранения
- Включить в v1 сетевую передачу или сохранение в локальную БД
Каждый пункт на первый взгляд выглядит удобным, но все они склонны утяжелять локализацию проблем и эксплуатацию.
Интеграционные тесты нужно продумывать с реальными файлами, реальными потоками и реальными процессами
Logger — компонент, в котором одни только юнит-тесты не дают уверенности. Проверка одного лишь форматирования строк и сериализации в JSON упускает то, что реально вызывает проблемы в проде: I/O, конкурентность, ротацию, flush при завершении и ошибки прав доступа.
Поэтому интеграционные тесты должны проверять с реальными файлами, реальными потоками и, где нужно, реальными процессами. Как минимум стоит избегать состояния «в обычные дни всё проходит, но во время инцидента доверять нельзя».
Пункты интеграционных тестов, которые стоит прогнать
Целостность одиночной записи
- Является ли каждая строка ровно одной JSON-записью
- Читается ли она обратно как
UTF-8 - Присутствуют ли обязательные поля каждый раз
- Не сломала ли встроенный перенос строки запись на несколько строк
Конкурентность в пределах одного процесса
- Остаются ли записи целостными при одновременной записи из нескольких потоков
- Нет ли недостачи или избытка числа записей
- При использовании очереди — соответствуют ли порядок и потери спецификации
Поведение flush и при завершении
- Отражаются ли
Error/Criticalнемедленно - Пуста ли очередь после штатного завершения
- Сохраняются ли необходимые финальные записи на путях, близких к аварийному завершению
Ротация и хранение
- Переключается ли логгер на новый файл при выполнении условия ротации
- Удаляются ли старые файлы сверх лимита хранения согласно спецификации
- Остаются ли JSON-строки целостными непосредственно до и после ротации
Аварийные сценарии
- Поведение при отсутствии директории назначения
- Поведение при отсутствии прав на запись
- Уведомление или возвращаемое значение при сбое записи в условиях, близких к переполнению диска
- Поведение при переполнении очереди
Работа с несколькими процессами
Если спецификация предполагает один файл на процесс, то сам факт, что другой процесс не пытается войти в тот же файл, может стать объектом проверки. И наоборот, при схеме с процессом агрегации проверка должна включать и сбои передачи данных этому процессу.
Минимальное число тестов, которое стоит прогнать в v1
Если пытаться сразу сделать всё, тесты становятся слишком тяжёлыми. Минимум для прохождения в v1 — примерно вот эти шесть.
- Нормальная запись из одного потока
- Одновременная запись из нескольких потоков
- Flush
Error/Critical - Ротация и хранение
- Уведомление о сбое при недоступности места назначения
- Drain и финальный flush при штатном завершении
Даже если проходят только эти шесть, вы уже далеко ушли от «логгера, который выводит строки, но которому нельзя доверять в эксплуатации».
Итог
Первая цель самописного logger — не богатство функций, а «то, чтобы ему можно было верить во время инцидента». Для этого эффективно зафиксировать формат как UTF-8 JSON Lines, держать набор обязательных полей компактным, сделать один файл на процесс базовым принципом и заранее решить вопросы flush, ротации, хранения и поведения при сбоях.
А действительно ли это проектирование работает, нужно проверять интеграционными тестами с реальными файлами, реальными потоками и реальными процессами. Прежде чем разрастить реализацию, стоит сначала закрепить минимальную конфигурацию и минимальный набор тестов — тогда впоследствии логгер получится развивать без лишнего напряжения.
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Где провести границу между юнит-тестами и интеграционными тестами
Разбираем границу между юнит-тестами и интеграционными тестами по осям чистой логики, форматов, связей, различий среды и зависимости от в...
Проектирование Windows-приложений, сохраняющих логи и дампы при сбое
Разбираем, как сочетать обычное логирование, финальный маркер сбоя, WER LocalDumps и процесс-наблюдатель, чтобы даже при падении Windows-...
Таблица решений: завершать работу приложения или продолжать при неожиданном исключении
Разбираем, когда после неожиданного исключения приложение стоит завершать, а когда можно продолжать работу — с точки зрения повреждения с...
Реагирование на инциденты не заканчивается восстановлением — шаблон постмортема (предотвращения повторения) для небольших команд разработки
Считать инцидент закрытым сразу после исправления и извинений — гарантированный способ повторить его снова. Адаптируем blameless-постморт...
Введение в ADR (Architecture Decision Record) — минимальный способ сохранить «почему мы спроектировали именно так» в небольшой команде
Код никогда не объясняет, почему он написан именно так. Разбираем, как использовать ADR (Architecture Decision Record) — одно решение, од...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Эта тема хорошо сочетается с упорядочиванием проектирования, реализации и эксплуатации логирования для Windows-инструментов и бизнес-приложений под реальные требования.
Технические консультации и ревью дизайна
Проработка формата логов, ротации, поведения при сбоях и объёма интеграционных тестов до реализации сама по себе — естественная тема для технической консультации.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Какой формат стоит использовать для самописного логгера?
- UTF-8 JSON Lines с одной записью на строку — удобная золотая середина. Простая конкатенация текста плохо поддаётся машинной обработке впоследствии, а тяжёлый собственный бинарный формат снижает наблюдаемость в эксплуатации. При JSON Lines файл остаётся читаемым как текст, легко разбирается скриптами и инструментами, а даже если запись обрывается на середине, легко понять, какая именно строка повреждена. Минимальный набор полей — timestamp, level, category, message, структурированные fields, sessionId и processId.
- Запись логов должна быть синхронной или асинхронной?
- Стратегию стоит выбирать по нагрузке. Пока объём логов невелик, синхронная запись понятнее и облегчает расследование сбоев; принудительная асинхронность рискует потерять записи прямо перед завершением работы. Если объём растёт и синхронный I/O становится узким местом, стоит перейти на single writer с ограниченной очередью и заранее решить политику на случай переполнения, а не оставлять её неопределённой. В любом случае записи Error и Critical, а также логи начала и конца сессии стоит сбрасывать (flush) синхронно.
- Что должен делать логгер, если не может записать в место назначения?
- Не переключаться молча на другое место хранения. Проектирование, при котором логгер тихо пишет куда-то ещё, если пункт назначения недоступен, затрудняет последующее расследование: сам факт отсутствия логов там, где они должны быть, задерживает первую реакцию эксплуатации на инцидент. Вместо этого нужно явно проявить сбой через видимый канал — уведомление внутри приложения, журнал событий Windows или стандартный поток ошибок, чтобы никто не гадал, куда делись логи.
- Почему самописному логгеру нужны именно интеграционные тесты, а не только юнит-тесты?
- Проверка одного только форматирования строк и сериализации в JSON упускает то, что реально вызывает проблемы в проде: I/O, конкурентность, ротацию, flush при завершении и ошибки прав доступа. Интеграционные тесты должны использовать реальные файлы, реальные потоки и, где нужно, реальные процессы. Минимальный набор, который стоит покрыть в v1, — шесть пунктов: нормальная запись из одного потока, одновременная запись из нескольких потоков, flush записей Error и Critical, ротация и хранение, уведомление о сбое при недоступности места назначения, а также drain и финальный flush при штатном завершении.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки