Перейти к основному содержимому

4.14. Настройка логирования

Разработчику Архитектору Инженеру

Настройка логирования

Что такое логирование?

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

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

А если речь идёт о большом приложении, то когда что-то пойдёт не так, и придётся разбираться, вам, как разработчику, понадобится набор каких-то данных, доказательств того, что сломалось. Да, система "не работает", но нужно ведь понять, в какой именно части кода, в каком именно модуле возникла ошибка - и именно для этого используется логирование. Чтобы записать информацию подробно и в дальнейшем получить возможность быстро найти пробелы.


Как реализуют логирование

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

  • Вывод в консоль — самый простой способ, используемый в терминале (для серверных приложений) или в консоли браузера (для клиентских JavaScript-приложений). Подходит для разработки и временного отладочного вывода.
  • Отображение на интерфейсе — используется в десктопных или мобильных приложениях, где логи показываются в специальной панели или окне. Применяется редко, только в отладочных сборках.
  • Сохранение в файл — стандартный подход для продакшн-систем. Логи пишутся в текстовые файлы (.txt, .log) или структурированные форматы (JSON, CSV), что позволяет легко их обрабатывать и анализировать.
  • Отправка на сервер — централизованное логирование, когда данные отправляются на удалённый сервер (например, через HTTP, syslog или специализированные протоколы). Используется в распределённых системах.
  • Сохранение в базу данных — применяется, когда требуется быстрый поиск, фильтрация или долгосрочное хранение. Может быть как в реляционной (SQL), так и в документной (NoSQL) базе.

Алгоритмическим языком можно представить такой пример:

алг Пример_логирования
нач
вызов ИнициализироватьЛог("приложение.log")
вызов ЗаписатьЛог("INFO", "Приложение запущено")
если ОшибкаПроизошла то
вызов ЗаписатьЛог("ERROR", "Произошла ошибка при обработке данных")
все
вызов ЗаписатьЛог("INFO", "Приложение завершает работу")
кон

Здесь основной алгоритм - инициализируется лог, определяется место сохранения, и конечно вызывается функция записи. В такую функцию передают текст, который нужно записать. В реальных языках программирования (Python, C#, Java, JavaScript) логирование реализуется через библиотеки или встроенные средства.

В результате, в зависимости от способа, мы получим лог в соответствующем формате:

Вывод в консоли

2026-03-15T14:23:01.123Z [INFO] UserService: Пользователь вошёл в систему. userId=12345, ip=192.168.1.100
2026-03-15T14:23:05.456Z [WARN] PaymentService: Таймаут при подключении к шлюзу. gateway=stripe, attempt=1
2026-03-15T14:23:07.789Z [ERROR] OrderService: Не удалось создать заказ. orderId=null, reason=InvalidPaymentToken

Особенности:

  • Каждая запись — одна строка.
  • Уровень выделен ([INFO], [WARN], [ERROR]).
  • Имя компонента (UserService) помогает быстро определить источник.
  • Контекст передаётся как пары ключ=значение — это позволяет легко парсить.

Файл .log или .txt

Лог-файл может быть как в plain text, так и в JSON. Вот оба варианта.

2026-03-15 14:23:01,123 | INFO  | UserService        | Пользователь вошёл в систему. userId=12345, ip=192.168.1.100
2026-03-15 14:23:05,456 | WARN | PaymentService | Таймаут при подключении к шлюзу. gateway=stripe, attempt=1
2026-03-15 14:23:07,789 | ERROR | OrderService | Не удалось создать заказ. orderId=null, reason=InvalidPaymentToken
2026-03-15 14:23:10,012 | DEBUG | CacheService | Кэш обновлён. key=user:12345, size=2.1KB
{"timestamp":"2026-03-15T14:23:01.123Z","level":"INFO","logger":"UserService","message":"Пользователь вошёл в систему","userId":12345,"ip":"192.168.1.100"}
{"timestamp":"2026-03-15T14:23:05.456Z","level":"WARN","logger":"PaymentService","message":"Таймаут при подключении к шлюзу","gateway":"stripe","attempt":1}
{"timestamp":"2026-03-15T14:23:07.789Z","level":"ERROR","logger":"OrderService","message":"Не удалось создать заказ","orderId":null,"reason":"InvalidPaymentToken"}
{"timestamp":"2026-03-15T14:23:10.012Z","level":"DEBUG","logger":"CacheService","message":"Кэш обновлён","key":"user:12345","sizeBytes":2150}

Таблица с записями логов

Если логи хранятся в реляционной базе данных (например, PostgreSQL, MySQL), они могут быть представлены в виде таблицы. Структура типична:

idtimestamplevelloggermessageuser_idiptrace_id
12026-03-15 14:23:01.123INFOUserServiceПользователь вошёл в систему12345192.168.1.100a1b2c3d4-e5f6-7890-g1h2...
22026-03-15 14:23:05.456WARNPaymentServiceТаймаут при подключении к шлюзуNULLNULLa1b2c3d4-e5f6-7890-g1h2...
32026-03-15 14:23:07.789ERROROrderServiceНе удалось создать заказ12345192.168.1.100a1b2c3d4-e5f6-7890-g1h2...
42026-03-15 14:23:10.012DEBUGCacheServiceКэш обновлён12345NULLb2c3d4e5-f6g7-8901-h2i3...

Пояснение полей:

  • id — уникальный идентификатор записи.
  • timestamp — точное время события.
  • level — уровень важности.
  • logger — имя модуля или класса.
  • message — читаемое описание.
  • user_id, ip — контекстные данные (могут быть NULL).
  • trace_id — идентификатор цепочки запросов (для распределённого трейсинга).

Такой формат позволяет выполнять SQL-запросы:

SELECT * FROM logs
WHERE level = 'ERROR'
AND timestamp > '2026-03-15 14:00:00'
ORDER BY timestamp DESC;

Как подключать и настраивать эти виды логирования

Подключение логирования начинается с выбора подходящего механизма записи и его конфигурации. В большинстве языков используются встроенные или сторонние библиотеки, которые позволяют гибко настраивать:

  • уровень логирования (debug, info, warning, error, critical);
  • формат сообщения;
  • место назначения (файл, консоль, сеть и т.д.);
  • ротацию файлов (автоматическое создание новых файлов по размеру или времени).

Например, в Python через logging.basicConfig() можно задать всё сразу. В C# часто используется ILogger из .NET с провайдерами (console, file, etc.). В JavaScript — либо console.log(), либо библиотеки вроде winston или pino.


Распространённые практики

  • Использование уровней логирования — не все события одинаково важны. Debug-сообщения нужны только при разработке, а error/critical — всегда.
  • Не логировать чувствительные данные — пароли, токены, персональные данные не должны попадать в логи.
  • Структурированный формат — JSON предпочтительнее обычного текста, так как его проще парсить и анализировать автоматически.
  • Контекст в логах — каждое сообщение должно содержать достаточно информации: время, модуль, пользователь (если применимо), идентификатор запроса.
  • Ротация и архивация — логи не должны занимать всё дисковое пространство. Используются политики ротации и сжатия старых файлов.
  • Централизованное хранение — в микросервисных архитектурах логи всех сервисов собираются в одном месте (например, через ELK-стек или Loki+Grafana).

Алгоритм подключения логирования

  1. Выбор библиотеки или встроенного средства — определить, что использовать: встроенное API или стороннюю библиотеку.
  2. Определение уровней логирования — решить, какие уровни будут активны в разных окружениях (dev, test, prod).
  3. Настройка формата сообщений — установить шаблон: время, уровень, имя модуля, сообщение.
  4. Выбор места назначения — консоль, файл, база данных, внешний сервис.
  5. Добавление контекста — обеспечить передачу идентификаторов сессий, пользователей, запросов.
  6. Тестирование — убедиться, что логи пишутся корректно и содержат нужную информацию.
  7. Настройка ротации и мониторинга — автоматизировать управление объёмом логов и настроить алерты на критические ошибки.

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

  • Python: встроенная библиотека logging достаточно мощна для большинства задач. Для продвинутого использования — structlog, loguru.
  • JavaScript (Node.js): console подходит для прототипов, но для продакшена лучше winston, pino, bunyan.
  • C# (.NET): ILogger<T> из Microsoft.Extensions.Logging — стандарт де-факто. Часто используется вместе с Serilog или NLog для расширенных возможностей.
  • Java: java.util.logging — встроенный, но редко используется. Предпочтительны SLF4J + Logback или Log4j2.

Уровни логирования позволяют фильтровать сообщения по важности. Стандартные уровни (в порядке возрастания серьёзности):

  • DEBUG — детальная информация, интересная только при диагностике.
  • INFO — общие события жизненного цикла (запуск, завершение, успешные операции).
  • WARNING / WARN — потенциально проблемные ситуации, не приводящие к сбою.
  • ERROR — ошибка, повлиявшая на выполнение операции, но не упавшая система.
  • CRITICAL / FATAL — критическая ошибка, требующая немедленного вмешательства.

Настройка уровней зависит от окружения:

  • Разработка (dev): DEBUG — все сообщения.
  • Тестирование (test): INFO или WARN — чтобы видеть основные события без шума.
  • Продакшен (prod): WARN или ERROR — только значимые события, чтобы не перегружать диск и не раскрывать внутренности системы.

В конфигурационных файлах (например, appsettings.json в .NET или logback.xml в Java) уровни задаются явно для каждого компонента или глобально.

Формат определяет, как выглядит каждая запись. Хороший формат включает:

  • временная метка (ISO 8601: 2026-03-15T14:23:01.123Z);
  • уровень (INFO, ERROR);
  • имя модуля/класса/файла;
  • идентификатор запроса или корреляции (trace ID);
  • само сообщение;
  • дополнительные поля (пользователь, IP, версия ПО).

Пример структурированного JSON-лога:

{
"timestamp": "2026-03-15T14:23:01.123Z",
"level": "INFO",
"logger": "UserService",
"message": "Пользователь вошёл в систему",
"userId": 12345,
"traceId": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8"
}

В Python через logging.Formatter можно задать:

formatter = logging.Formatter(
'%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)

В Serilog (C#):

.WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {SourceContext}: {Message}{NewLine}{Exception}")

Структурированный формат (особенно JSON) критически важен для автоматического парсинга в системах вроде ELK, Grafana Loki, Datadog.

Логи могут направляться в разные «приёмники» (sinks):

  • Консоль — удобно при разработке и в контейнерах (Docker), где stdout/stderr захватываются оркестратором.
  • Файл — классический способ. Используется ротация по размеру или времени (app.log, app.2026-03-15.log).
  • Системный журнал (syslog, journald) — в Linux-средах.
  • База данных — редко, так как создаёт нагрузку; применяется для аудита или когда нужен SQL-доступ.
  • Сетевой эндпоинт — HTTP, TCP, UDP, syslog, или специализированные протоколы (например, Fluentd, OpenTelemetry).
  • Облачные сервисы — AWS CloudWatch, Azure Monitor, Google Cloud Logging.

Часто используется мультиплексирование: одновременная запись в консоль и файл, или в файл и удалённый сервер.

Без контекста логи бесполезны. Контекст помогает связать события между собой:

  • Trace ID — уникальный идентификатор запроса, проходящего через микросервисы.
  • User ID / Session ID — кто инициировал действие.
  • IP-адрес клиента — откуда пришёл запрос.
  • Версия приложения — для диагностики регрессий.
  • География, язык, устройство — если релевантно.

В веб-приложениях контекст часто берётся из HTTP-заголовков или middleware. Например, в ASP.NET Core используется ILogger.BeginScope(), в Express.js — middleware, который добавляет req.id в каждый лог.

Пример (Python + structlog):

log = structlog.get_logger()
with structlog.contextvars.bind_contextvars(user_id=123, trace_id="abc123"):
log.info("action performed")

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

  • логи действительно пишутся;
  • содержат правильный уровень и формат;
  • контекст присутствует;
  • чувствительные данные не попадают в логи;
  • при ошибке записывается стек вызовов.

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

Для продакшена — проверить, что логи поступают в централизованную систему (если используется).

Без управления объёмом логи быстро заполнят диск. Ротация — это автоматическое создание новых файлов и удаление старых.

  • По размеру: новый файл после 100 МБ.
  • По времени: новый файл каждый день.
  • По количеству: хранить не более 7 файлов.

В Python — RotatingFileHandler или TimedRotatingFileHandler.
В .NET — Serilog.Sinks.File с параметром rollOnFileSizeLimit.
В Java — <rollingPolicy> в logback.xml.

Мониторинг — это настройка алертов на критические события:

  • частые ERROR или CRITICAL;
  • рост объёма логов (может указывать на зацикливание);
  • отсутствие логов (сервис упал).

Интеграция с Prometheus + Alertmanager, Grafana, или облачными мониторинговыми сервисами позволяет получать уведомления в Slack, Telegram, email.

Этот алгоритм применим ко всем языкам и платформам.


Как проверять и читать логи

  • Локальные файлы: читаются через текстовые редакторы, tail -f в терминале, или специализированные просмотрщики (например, LogMX, Chainsaw).
  • Консоль браузера: вкладка Console в DevTools.
  • Серверные логи: через SSH, journalctl, docker logs, или централизованные системы (Kibana, Grafana Logs).
  • Базы данных: SQL-запросы или NoSQL-поиск по полям.
  • Фильтрация: по времени, уровню, ключевым словам, идентификаторам.

Хорошая практика — использовать grep, awk, jq (для JSON) или SIEM-системы для анализа больших объёмов логов.

Логи, записанные в файлы на диске, являются наиболее распространённым способом хранения событий. Они могут быть как простыми текстовыми файлами (.log, .txt), так и структурированными (JSON, CSV).

Способы просмотра:

  • Текстовые редакторы: Notepad++, VS Code, Sublime Text — удобны для небольших файлов.
  • Командная строка:
    • cat app.log — вывод всего содержимого файла.
    • less app.log — постраничный просмотр с возможностью прокрутки.
    • tail -f app.log — отображение последних строк файла в реальном времени (полезно при активной записи).
    • head -n 20 app.log — показ первых 20 строк.

Специализированные просмотрщики:

  • LogMX — графический инструмент с подсветкой уровней, фильтрацией и поддержкой регулярных выражений.
  • Chainsaw — утилита от Elastic, предназначенная для быстрого поиска по логам с использованием Sigma-правил.
  • lnav — терминальный просмотрщик с автоматическим определением формата и подсветкой.

Для структурированных логов (например, JSON) особенно полезны инструменты, поддерживающие парсинг и фильтрацию по полям.

В веб-разработке логи часто выводятся в консоль браузера через console.log(), console.error() и другие методы. Это помогает отслеживать состояние приложения во время работы.

Как читать:

  • Открыть DevTools (F12 или ПКМ → «Просмотреть код»).
  • Перейти во вкладку Console.
  • Использовать фильтры по уровням: Info, Warning, Error, Debug.
  • Искать по ключевым словам с помощью панели поиска (Ctrl+F).

Хорошая практика — не оставлять console.log() в продакшен-коде, так как это может раскрыть внутреннюю логику и замедлить работу приложения.

На серверах логи могут храниться в разных местах в зависимости от ОС, сервиса и способа развёртывания.

Основные способы доступа:

  • SSH: подключение к удалённому серверу и работа с файлами через терминал.
  • systemd/journald:
    • journalctl -u myapp.service — просмотр логов конкретного systemd-сервиса.
    • journalctl --since "1 hour ago" — фильтрация по времени.
  • Docker:
    • docker logs <container_id> — вывод всех логов контейнера.
    • docker logs -f <container_id> — потоковый просмотр (аналог tail -f).
    • docker logs --since 1h <container_id> — логи за последний час.

Централизованные системы:

  • Kibana — визуальный интерфейс для Elasticsearch, позволяет строить дашборды, фильтровать по полям, искать по шаблонам.
  • Grafana Logs (Loki) — легковесная система логирования, интегрированная с Grafana, использует метки вместо полнотекстового индекса.
  • Datadog, New Relic, Sentry — облачные платформы с продвинутыми возможностями поиска, алертинга и корреляции с метриками.

Некоторые системы пишут логи напрямую в базы данных — чаще всего для аудита, аналитики или долгосрочного хранения.

Чтение из SQL-баз:

SELECT * FROM application_logs
WHERE level = 'ERROR'
AND created_at > '2026-03-15 10:00:00'
ORDER BY created_at DESC;

Чтение из NoSQL (например, MongoDB):

db.logs.find({
level: "ERROR",
timestamp: { $gt: ISODate("2026-03-15T10:00:00Z") }
}).sort({ timestamp: -1 });

Преимущества такого подхода — мощные возможности фильтрации и агрегации. Недостаток — нагрузка на базу и сложность масштабирования.

Фильтрация — ключевой навык при работе с логами. Без неё легко утонуть в потоке данных.

Основные критерии фильтрации:

  • Время: события за последние 5 минут, час, день.
  • Уровень: только ERROR и CRITICAL.
  • Ключевые слова: timeout, auth failed, NullPointerException.
  • Идентификаторы: trace_id, user_id, request_id — позволяют отследить цепочку событий.

Для обработки тысяч или миллионов строк применяются специализированные утилиты:

  • grep — поиск по шаблону:

    grep "ERROR" app.log
    grep -i "timeout" app.log # без учёта регистра
  • awk — обработка колонок и условий:

    awk '$3 == "ERROR" { print $0 }' app.log
  • jq — парсинг и фильтрация JSON:

    cat app.jsonl | jq 'select(.level == "ERROR") | .message'
  • SIEM-системы (Security Information and Event Management):

    • Splunk, IBM QRadar, Elastic SIEM — позволяют собирать, коррелировать и визуализировать логи из множества источников.
    • Поддерживают правила обнаружения аномалий, автоматические алерты и расследование инцидентов.

Эти инструменты особенно важны в распределённых системах, где один запрос проходит через десятки сервисов, и логи разбросаны по разным машинам.


Как правильно записывать логи

  • Используйте интерполяцию или форматирование, а не конкатенацию строк:

    logger.info(f"Пользователь {user_id} выполнил действие '{action}'")

    вместо

    logger.info("Пользователь " + str(user_id) + " выполнил действие '" + action + "'")
  • Не логируйте объекты напрямую — преобразуйте их в читаемый или сериализуемый вид:

    _logger.LogInformation("Запрос: {@Request}", request);

    (в Serilog @ означает структурированную сериализацию)

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

  • Добавляйте метаданные: время, уровень, имя потока, идентификатор трассировки.

  • Используйте параметризованные сообщения, если библиотека это поддерживает:

    logger.info("Обработка заказа {} для клиента {}", orderId, customerId);

Библиотеки и примеры реализации

Python (logging + structlog)

import logging
import structlog

# Базовая настройка
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
handlers=[
logging.FileHandler("app.log"),
logging.StreamHandler()
]
)

logger = logging.getLogger(__name__)
logger.info("Приложение запущено")

# Структурированное логирование
structlog.configure(
processors=[
structlog.processors.JSONRenderer()
],
logger_factory=structlog.stdlib.LoggerFactory(),
)
log = structlog.get_logger()
log.info("event", user_id=123, action="login")

JavaScript (Node.js, winston)

const winston = require('winston');

const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.Console()
]
});

logger.info('Приложение запущено', { userId: 123 });

C# (.NET, ILogger + Serilog)

using Microsoft.Extensions.Logging;
using Serilog;

Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/app.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddLogging(logging => {
logging.AddSerilog();
});

var app = builder.Build();
var logger = app.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Приложение запущено. Пользователь: {UserId}", userId);

Java (SLF4J + Logback)

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App {
private static final Logger logger = LoggerFactory.getLogger(App.class);

public static void main(String[] args) {
logger.info("Приложение запущено. Пользователь: {}", userId);
}
}

Файл logback.xml:

<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.contrib.json.classic.JsonLayout"/>
</encoder>
</appender>
<root level="info">
<appender-ref ref="FILE"/>
</root>
</configuration>

Какие данные записывать в логи, а какие — нет

Записывать стоит:

  • Время события;
  • Уровень важности (info, error и т.д.);
  • Идентификатор операции или запроса (trace ID);
  • Имя модуля или класса;
  • Краткое описание события;
  • Контекст: ID пользователя, IP-адрес, тип устройства;
  • Параметры вызова (без чувствительных данных);
  • Стек вызовов при ошибках.

Не записывать:

  • Пароли, токены, ключи API;
  • Персональные данные (ФИО, email, телефон) без согласия и шифрования;
  • Полные тела запросов/ответов, если они содержат приватную информацию;
  • Дублирующие или избыточные сообщения (например, лог каждой итерации цикла без причины);
  • Бинарные данные или огромные структуры без обрезки.

Правильное логирование — это баланс между информативностью и безопасностью. Логи должны помогать, а не мешать или создавать риски.