1. Структура лог‑повідомлення
Уявімо, що логи — це не просто «потік свідомості» вашого застосунку, а цінний журнал, у якому за місяць чи рік ви або ваш колега зможете знайти відповідь на запитання: «Що ж тут, чорт забирай, сталося?». Щоб це було можливим, кожне лог‑повідомлення має бути структурованим. Зазвичай (і це стандарт у більшості бібліотек) кожне повідомлення містить:
- Час події — коли це сталося.
- Рівень — наскільки це важливо (INFO, ERROR тощо).
- Ім’я логера — зазвичай це ім’я класу або компонента.
- Текст повідомлення — що саме сталося.
- Стек‑трейс (якщо є помилка) — щоб зрозуміти, де й чому.
Ось приклад добре відформатованого рядка лога (на прикладі Log4j/SLF4J):
2024-06-16 18:42:07,123 INFO com.example.MainApp - Користувач увійшов у систему: username=vasya
А якщо сталася помилка:
2024-06-16 18:42:10,456 ERROR com.example.LoginService - Помилка автентифікації користувача: vasya
java.lang.IllegalArgumentException: Неправильний пароль
at com.example.LoginService.checkPassword(LoginService.java:42)
...
Чому це важливо?
Коли застосунок працює довго, логи можуть займати гігабайти. Якщо повідомлення не структуровані, знайти проблему стає завданням рівня «вгадай мелодію за шумом вентилятора».
2. Форматування повідомлень
Чому не варто робити так:
logger.info("Користувач " + username + " увійшов у систему");
Здавалося б, усе просто, але є підводний камінь: навіть якщо рівень логування зараз встановлено на ERROR, рядок усередині дужок однаково буде сформовано (конкатенація відпрацює), а це — зайві витрати ресурсів. У великих системах, де логів тисячі рядків на секунду, це може виливатися в реальні затримки.
Як правильно: шаблони та параметри
Сучасні бібліотеки (наприклад, SLF4J і Log4j 2) підтримують шаблони з параметрами:
logger.info("Користувач {} увійшов у систему", username);
Тут рядок буде сформовано лише якщо рівень логування дозволяє вивести це повідомлення. Якщо зараз, наприклад, встановлено WARN, то не відбудеться навіть формування рядка — економія ресурсів і нервів.
Бонус: якщо передати кілька параметрів, їх буде підставлено по черзі:
logger.info("Користувач {} виконав дію {} над об’єктом {}", username, action, objectId);
Логування винятків (стек‑трейс)
Якщо ви перехоплюєте виняток, не потрібно вручну додавати стек‑трейс до повідомлення:
// НЕ ПОТРІБНО:
logger.error("Помилка: " + ex.getMessage() + "\n" + Arrays.toString(ex.getStackTrace()));
Правильно:
logger.error("Помилка під час обробки запиту", ex);
SLF4J і Log4j самі коректно додадуть стек‑трейс у лог.
Приклад: порівняння підходів
// Погано (конкатенація завжди виконується)
logger.debug("Об’єкт: " + expensiveToString(obj));
// Добре (ліниве формування)
logger.debug("Об’єкт: {}", obj);
3. Вибір рівнів логування
Якщо у вас у логах усе підряд на рівні ERROR, то це вже не логи, а «червона лампочка». Якщо все — на DEBUG, ви потонете в деталях. Розберімося, коли який рівень використовувати.
| Рівень | Для чого використовується | Приклад повідомлення |
|---|---|---|
|
Критичні збої, через які система працює некоректно або не працює взагалі | «Помилка підключення до бази даних» |
|
Важливі попередження, які не критичні, але потребують уваги | «Не вдалося знайти користувача, використано guest» |
|
Звичайні події, що відображають нормальну роботу застосунку | «Користувача зареєстровано: vasya» |
|
Детальна інформація для налагодження, не потрібна у продакшені | «Викликано метод checkPassword з параметрами ...» |
|
Найдетальніша інформація, зазвичай для глибинної діагностики | «Початок циклу обробки: i=0» |
Приклади типових повідомлень
- ERROR — не вдалося записати файл, перехоплено необроблений виняток, сервіс недоступний.
- WARN — застарілий API, підозріла поведінка користувача, перевищено ліміт спроб.
- INFO — користувач увійшов/вийшов, завершено обробку замовлення, запуск застосунку.
- DEBUG — параметри запиту, значення змінних, проміжні результати обчислень.
- TRACE — вхід/вихід у методи, внутрішні цикли, подробиці роботи алгоритмів.
Порада:
У продакшені зазвичай вмикають лише INFO і вище, іноді WARN і ERROR. DEBUG і TRACE — лише під час пошуку складних багів.
4. Best practices (найкращі практики логування)
Не логуйте чутливі дані
Паролі, токени, номери кредитних карток — усьому цьому не місце в логах. Навіть якщо здається, що «лог‑файл — лише для мене», згадайте про GDPR та колегу, який випадково надішле лог у спільний чат.
// Погано:
logger.info("Користувач {} увійшов із паролем {}", username, password);
// Добре:
logger.info("Користувач {} увійшов у систему", username);
Не зловживайте рівнем ERROR
Якщо ви пишете все підряд через logger.error, то коли справді станеться катастрофа, цього ніхто не помітить — усі звикнуть до «червоних лампочок». Використовуйте ERROR лише для ситуацій, коли застосунок справді не може продовжувати роботу або порушено бізнес‑логіку.
Логуйте винятки з повним стеком
Не пишіть лише ex.getMessage(), інакше ви ніколи не дізнаєтеся, де саме сталася помилка. Передавайте виняток другим параметром у логер.
logger.error("Помилка під час обробки запиту", ex);
Використовуйте унікальні ідентифікатори (кореляція подій)
У великих системах корисно призначати кожному запиту, користувачу або операції унікальний ідентифікатор. Це допоможе «зшити» події з різних частин системи.
logger.info("Розпочато обробку замовлення: orderId={}", orderId);
logger.info("Замовлення успішно оброблено: orderId={}", orderId);
Не пишіть у логи все підряд
Якщо логів надто багато — вони стають марними. Не логуйте кожен рядок коду, інакше знайти потрібну інформацію буде неможливо.
Форматуйте повідомлення зрозуміло
Пишіть повідомлення так, щоб їх зрозумів не лише автор коду, а й людина, яка читатиме логи за пів року. Уникайте абревіатур, неочевидних скорочень і «жартів для своїх».
5. Практика: налаштування формату лога та рівнів
Приклад налаштування формату в Log4j2 (log4j2.xml)
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
Що це означає?
- %d{...} — час події.
- %-5level — рівень (ERROR, INFO тощо).
- %logger{36} — ім’я логера (зазвичай клас).
- %msg — саме повідомлення.
Приклад коду з різними рівнями логування (SLF4J)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogDemo {
private static final Logger logger = LoggerFactory.getLogger(LogDemo.class);
public static void main(String[] args) {
logger.info("Застосунок запущено");
logger.debug("Значення змінної x: {}", 42);
try {
throw new IllegalArgumentException("Ой-ой-ой!");
// ...
} catch (Exception ex) {
logger.error("Сталася помилка під час запуску", ex);
}
}
}
Демонстрація різниці між рівнями
Якщо в налаштуваннях логера встановлено рівень INFO, то повідомлення з рівнем DEBUG і нижче не виводитимуться. Спробуйте змінити рівень на debug у конфігурації — побачите подробиці.
6. Типові помилки
Помилка № 1: Конкатенація рядків у логах. Дуже часто новачки пишуть так:
logger.debug("Користувач: " + user.getName() + ", роль: " + user.getRole());
У результаті навіть за вимкненого рівня DEBUG ці рядки все одно формуються, що призводить до зайвого навантаження. Використовуйте параметри!
Помилка № 2: Логування без стека винятку. Записують лише повідомлення:
logger.error("Помилка: " + ex.getMessage());
У результаті в логах немає інформації, де саме сталася помилка. Передавайте виняток другим параметром!
Помилка № 3: Логування всього підряд на рівні ERROR. Якщо все червоне — нічого не червоне. Використовуйте рівні за призначенням, інакше важливі помилки загубляться серед «дрібниць».
Помилка № 4: Логування чутливих даних. Ніколи не пишіть у логи паролі, токени, номери карток. Навіть якщо здається, що ніхто не побачить, життя любить сюрпризи.
Помилка № 5: Незрозумілі повідомлення. Якщо повідомлення в логу виглядає як «ERR42: fail», то за місяць ви самі не згадаєте, що це означає. Пишіть зрозуміло й докладно.
Помилка № 6: Відсутність унікальних ідентифікаторів. У складних системах без orderId, userId та інших ідентифікаторів ви не зможете «зшити» події й зрозуміти, що відбувалося з конкретним користувачем або замовленням.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ