JavaRush /Курси /Spring Boot /Кінцева точка /actuator/lo...

Кінцева точка /actuator/loggers

Spring Boot
Рівень 21 , Лекція 3
Відкрита

1. Рівні логування в runtime

Уявіть ситуацію: ваш catalog-service уже працює, ви акуратно налаштували рівні логування й не перетворили консоль на водоспад. І тут раптом хтось — зазвичай ви ж, але «вчорашній» — помічає дивну поведінку: наприклад, фільтр publishedOnly=true поводиться не так, як очікувалося. Ви розумієте, що логів мало, але змінювати конфіг, перезбирати проєкт, перезапускати застосунок і знову відтворювати сценарій — це майже мініритуал викликання багів. А баги й так приходять без запрошення.

У такі моменти хочеться мати «ручку гучності» для логів. Не глобальну й не варіант «увімкнемо DEBUG всюди — і нехай світ горить», а точкову: підсилити діагностику лише в одному пакеті або навіть в одному класі, щоб побачити саме ті деталі, які потрібні. Саме це й дає endpoint loggers: він дозволяє під час роботи застосунку подивитися рівні логерів і тимчасово змінити їх без правки коду та без перезапуску.

Важливо відразу домовитися про терміни. Налаштування в YAML — це «постійна політика». Зміна через endpoint — це «тимчасовий діагностичний маневр». Це не заміна конфігурації, а її акуратне тимчасове перевизначення під час виконання, яке допомагає швидко зрозуміти проблему й потім прибрати сліди свого втручання, ніби вас у кімнаті й не було. Хоча ви, звісно, все чіпали.

2. Endpoint loggers в Actuator

loggers — це HTTP endpoint, який показує й дозволяє змінювати рівні логування в конкретних логерах. У світі Spring Boot це зазвичай реалізовано через Actuator. Але зараз нам не потрібно перетворювати лекцію на огляд усього Actuator: ми беремо один інструмент і вчимося користуватися ним акуратно, як викруткою, а не як бензопилою.

Тут нам потрібен рівно один шматок Actuator, тому що керування рівнями в runtime — це частина процесу логування. Але в цього інструмента є проста умова: endpoint з’явиться лише якщо в проєкті підключено spring-boot-starter-actuator, а сам loggers дозволений для HTTP exposure. Повну карту actuator endpoint’ів ми зараз не розкручуємо — для цієї задачі нам потрібна лише одна діагностична ручка.

За замовчуванням у невеликого Boot-застосунку рівні логування задаються через logging.level.* у YAML, як ми вже робили раніше. Endpoint loggers додає можливість подивитися поточний стан «як воно реально працює» і тимчасово підсилити рівень для вибраного логера. Точка входу в більшості проєктів має такий вигляд: /actuator/loggers. Якщо ви не змінювали base path, це стандартний шлях.

Є тонкість, без якої нічого не злетить: endpoint може бути «увімкнений», але не експонований через HTTP. У навчальному сервісі це особливо важливо, тому що ми не хочемо випадково відкрити все підряд. Тому навіть якщо ви не розглядаєте Actuator цілком, для loggers потрібно хоча б мінімально дозволити його в потрібному профілі. Приклад максимально малого й чесного налаштування для application-local.yaml може виглядати так:

# src/main/resources/application-local.yaml

# Дозволяємо через HTTP лише endpoint loggers (не вмикаємо все підряд).
management:
  endpoints:
    web:
      exposure:
        include: "loggers" # Рівно одна «точка діагностики» для локального налагодження.

Тут немає філософії, лише механіка: «я хочу саме цю точку». Жодних *, жодних «а давайте все, адже ми ж у локальному середовищі». Це як дати собі доступ рівно до однієї викрутки, а не до складу зі зброєю.

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

Що робимо HTTP URL Навіщо
Дивимося список логерів GET /actuator/loggers зрозуміти, які логери взагалі видимі
Дивимося один логер GET /actuator/loggers/{name} побачити рівні конкретного логера
Змінюємо рівень POST /actuator/loggers/{name} тимчасово поставити DEBUG/TRACE тощо

3. Ієрархія логерів: ROOT → package → class

Щоб loggers endpoint був зрозумілий, потрібно згадати одну важливу ідею: логери в Java зазвичай влаштовані ієрархічно. Тобто логер «для класу» знаходиться всередині логера «для пакета», а той — всередині ROOT. Якщо уявити це як родинне дерево, то ROOT — це прабатько, який за замовчуванням впливає на всіх, якщо ніхто не перевизначив правила ближче до листків.

У SLF4J/Logback (і в Spring Boot за замовчуванням) імʼя логера найчастіше збігається з повним імʼям класу або пакета. Наприклад, якщо у нас є клас com.example.catalogservice.catalog.service.CourseCatalogService, то логер цього класу називатиметься так само. А логер пакета може називатися com.example.catalogservice.catalog або com.example.catalogservice.catalog.service — і тоді все всередині нього успадковує рівень, доки ви не перевизначили точніше.

Дуже корисно уявляти це як «налаштування гучності в музичному мікшері». ROOT — загальний майстер-фейдер. Пакет — фейдер групи інструментів. Конкретний клас — фейдер однієї доріжки. І якщо ви поставите ROOT=DEBUG, це як викрутити загальний майстер на максимум: ви почуєте не лише «вашу гітару», а й те, як барабанщик дихає та як лампочка в студії гуде на 50 герцах.

Для catalog-service ієрархія може виглядати приблизно так:

ROOT
└─ com.example.catalogservice
   ├─ com.example.catalogservice.catalog
   │  ├─ com.example.catalogservice.catalog.web
   │  ├─ com.example.catalogservice.catalog.service
   │  └─ com.example.catalogservice.catalog.repository
   ├─ com.example.catalogservice.config
   └─ com.example.catalogservice.actuator

Звідси випливає простий, але потужний принцип: якщо вам потрібно більше деталей про ваш код, майже завжди правильніше підвищувати рівень у com.example.catalogservice.catalog або ще точніше, а не в ROOT. Це дає високий сигнал і низький шум — саме заради цього ми взагалі й починали логування.

4. configuredLevel vs effectiveLevel

Коли ви починаєте дивитися на дані endpoint’а loggers, там майже одразу трапляються два поля, які новачки регулярно плутають: configuredLevel і effectiveLevel. І це дуже зрозуміла плутанина: обидва слова виглядають так, ніби означають «рівень логування», але насправді відповідають на різні запитання.

configuredLevel каже: «який рівень явно задано саме для цього логера». Тобто це як ручне налаштування на конкретній доріжці. Якщо там null, це не помилка й не «рівень не визначено». Це означає: «на цьому рівні нічого явно не задано, отже рівень буде успадковано від батька».

effectiveLevel каже: «який рівень реально діє після врахування успадкування». Тобто це підсумок: те, що буде використано для ухвалення рішення, друкувати чи ні DEBUG-повідомлення.

Розглянемо дуже типову картину: у application.yaml ви задали root: INFO, а для пакета com.example.catalogservice.catalog нічого не задавали. Тоді запит:

GET /actuator/loggers/com.example.catalogservice.catalog

може повернути відповідь такого вигляду:

{
  "configuredLevel": null,
  "effectiveLevel": "INFO"
}

Це читається так: «для пакета явно нічого не налаштовано, але реально діє INFO, тому що воно прийшло від ROOT».

А тепер ви через endpoint тимчасово поставили DEBUG:

POST /actuator/loggers/com.example.catalogservice.catalog
Content-Type: application/json

{
  "configuredLevel": "DEBUG"
}

і повторний GET покаже:

{
  "configuredLevel": "DEBUG",
  "effectiveLevel": "DEBUG"
}

Тобто тепер у пакета є власне налаштування, і воно ж стає ефективним.

Для закріплення — маленька табличка:

Поле Що означає простими словами Чому це важливо
configuredLevel «явно задано тут» допомагає зрозуміти, хто саме перевизначає рівень
effectiveLevel «реально діє» саме це визначає, чи друкуватимуться DEBUG/TRACE

Якщо ви запамʼятаєте цю різницю, половина «чому це не логування» перестане бути містикою. Друга половина залишиться містикою, але вже чесною: «я просто забув, що логер називається не так, як я думав».

5. Тимчасове перевизначення рівня логування

Тепер зберемо все в один робочий сценарій, який виглядає як «пʼять хвилин тверезості посеред хаосу». В ідеальному світі ви спочатку дивитеся поточний стан логера, потім акуратно підсилюєте рівень, відтворюєте проблему, збираєте потрібні логи й одразу ж повертаєте все назад. Це схоже на те, як хірург бере інструмент, робить одну точну операцію й кладе інструмент на місце, а не починає жити з ним у руці.

Крок «подивитися стан» має такий вигляд:

GET /actuator/loggers/com.example.catalogservice.catalog

і ви читаєте effectiveLevel, щоб розуміти, що реально відбувається. Після цього, якщо потрібно підсилити сигнал, ви робите POST і ставите configuredLevel, наприклад, на DEBUG. Важливо, що ця зміна діє одразу й не потребує перезапуску.

POST /actuator/loggers/com.example.catalogservice.catalog
Content-Type: application/json

{
  "configuredLevel": "DEBUG"
}

Після діагностики — і ось тут починається доросле життя — ви повертаєте логер до успадкування, тобто скидаєте configuredLevel у null. Так, null тут — «поверни як було», а не «зламай усе».

POST /actuator/loggers/com.example.catalogservice.catalog
Content-Type: application/json

{
  "configuredLevel": null
}

І ще одна деталь, об яку люди спотикаються: зміна через endpoint не записується в YAML. Це runtime-стан. Він житиме до рестарту застосунку, а після рестарту повернеться політика з конфігурації. Тому такий override — чудовий тимчасовий захід, але погане «постійне налаштування». Якщо ви спіймали себе на тому, що щодня вручну ставите DEBUG одному й тому самому пакету, це сигнал: можливо, базову конфігурацію варто трохи підкрутити в профільних YAML, а не жити на ручному керуванні.

Щоб запамʼятати правильний цикл, можна тримати в голові міні-блок-схему:

flowchart TD
    %% Діагностичний цикл: посилили сигнал → подивилися → повернули як було.
    A["GET /actuator/loggers/{logger}"] --> B{"Сигналу вистачає?"}
    B -- так --> C["Нічого не змінюємо"]
    B -- ні --> D["POST configuredLevel=DEBUG"]
    D --> E["Відтворили проблему"]
    E --> F["Зібрали потрібні логи"]
    F --> G["POST configuredLevel=null"]
    G --> H["Повернулися до базової політики"]

І так: спокуса поставити ROOT=DEBUG буде завжди. Це як натиснути кнопку «Зробити все добре». Але насправді це частіше кнопка «Зробити шумно». Майже завжди починайте з вузького пакета. Якщо не вистачає — розширюйте зону. І лише якщо ви вже у відчаї та маєте зайвий диск під логи, тоді можна подумати про ROOT.

6. Мінісценарій у catalog-service: DEBUG для каталогу

Щоб усе це не залишилося абстракцією, уявімо, що нам потрібно зрозуміти, чому фільтрація курсів іноді повертає «занадто багато» результатів. Ми не хочемо друкувати деталі фільтрації завжди, тому що це буде шумно, але для діагностики нам потрібні кілька додаткових рядків.

Додамо один діагностичний DEBUG-лог у сервіс каталогу. Він має бути коротким і «по справі»: які вхідні параметри прийшли й скільки результатів вийшло. Для Junior’а важливо памʼятати, що DEBUG — це не «поганий лог», а просто «детальний лог». Поганим він стає, коли ви починаєте пхати туди все підряд.

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

public class CourseCatalogService {

    // Логер зазвичай створюють для класу: так імʼя логера збігається з імʼям класу.
    private static final Logger log = LoggerFactory.getLogger(CourseCatalogService.class);

    public void logFiltering(String track, String level, boolean publishedOnly) {
        // У DEBUG пишемо лише те, що допоможе швидко зрозуміти вхідні параметри й результат.
        // Плейсхолдери {} важливі: рядок збирається ліниво і не витрачає ресурси, якщо DEBUG вимкнено.
        log.debug("Фільтрація курсів: track={}, level={}, publishedOnly={}",
                track, level, publishedOnly);
    }
}

Так, приклад спеціально винесено в маленький метод, щоб ми не роздували код. У реальному CourseCatalogService ви викличете це поруч із фільтрацією, де у вас уже є параметри. Важливо інше: за рівня INFO цей лог мовчить, а за DEBUG — з’являється.

Тепер відтворюємо картину. Припустімо, ви робите запит:

GET /api/catalog/courses?track=SPRING&publishedOnly=true

За стандартного рівня INFO ви цього DEBUG не побачите взагалі. І це нормально.

Далі ви вирішуєте: «Окей, на пʼять хвилин я хочу бачити подробиці лише для каталогу». Спочатку дивимося поточний стан:

GET /actuator/loggers/com.example.catalogservice.catalog

Якщо відповідь каже, що effectiveLevel зараз INFO, ми ставимо тимчасовий override на пакет com.example.catalogservice.catalog, а не на ROOT:

POST /actuator/loggers/com.example.catalogservice.catalog
Content-Type: application/json

{
  "configuredLevel": "DEBUG"
}

І тепер, коли ви повторите той самий запит до каталогу, ви почнете бачити додаткову діагностику. Особливо красиво це працює разом із MDC з минулої лекції: якщо у вас у web-шарі вже є requestId у MDC, то і INFO, і DEBUG повідомлення матимуть один і той самий ідентифікатор, і ви зможете зібрати історію конкретного запиту.

Наприклад, за structured logging ви можете отримати рядок приблизно такого вигляду:

{
  "level": "DEBUG",
  "message": "Фільтрація курсів: track=SPRING, level=null, publishedOnly=true",
  "logger_name": "com.example.catalogservice.catalog.service.CourseCatalogService",
  "requestId": "req-42"
}

Тут важливо не те, які саме поля має формат — вони залежать від ECS/GELF/Logstash, — а те, що «детальний рядок» з’явився лише тоді, коли ви його справді попросили, і зникне так само легко.

Після того як ви зрозуміли проблему, ви скидаєте override назад:

POST /actuator/loggers/com.example.catalogservice.catalog
Content-Type: application/json

{
  "configuredLevel": null
}

І застосунок повертається до базової політики логування, ніби нічого й не було. Логи чисті, консоль не кричить, ви молодець, а баг — уже ні.

7. Типові помилки під час роботи з /actuator/loggers

Перший час loggers endpoint сприймається як «клас, я можу вмикати DEBUG коли хочу». І це правда… але приблизно так само правда, що можна їсти торт на сніданок щодня. Можна, але наслідки будуть помітні. Помилки тут зазвичай не про синтаксис запиту, а про стиль: куди й наскільки ви підсилюєте логування, і чи вмієте ви повертати систему до нормального стану.

Помилка №1: одразу викручувати ROOT на DEBUG.
Це найчастіша реакція: «нічого не зрозуміло — увімкну DEBUG всюди». У результаті ви отримуєте тонни framework-логів, втрачаєте потрібний сигнал і починаєте читати консоль як роман на 1200 сторінок без змісту. Набагато ефективніше почати з вузького пакета застосунку й розширюватися лише якщо справді бракує даних.

Помилка №2: плутати configuredLevel і effectiveLevel та робити хибні висновки.
Новачок бачить configuredLevel=null і думає: «логер не налаштований, отже нічого не працює». А потім починає ставити рівні де попало. Правильна думка: configuredLevel відповідає за «явно задано тут», а реальну картину показує effectiveLevel. Спочатку дивимося effectiveLevel, потім ухвалюємо рішення.

Помилка №3: забути скинути тимчасове налаштування назад.
Ви вмикнули DEBUG, знайшли проблему, пораділи… і залишили так на весь день. Потім ви дивуєтеся, чому в логах шум, чому файл логів росте як дріжджове тісто, і чому колеги починають дивитися на вас із підозрою. Тимчасовий override має бути саме тимчасовим: подивилися → підсилили → розібралися → скинули.

Помилка №4: використовувати runtime override як «постійну конфігурацію».
Якщо ви ловите себе на думці «я щоразу після старту ставлю цей логер у DEBUG через endpoint», то це вже не діагностика, а незручна рутина. Постійні правила мають жити в YAML профілів (local/dev/prod), інакше ви перетворюєте поведінку застосунку на набір ручних дій, які неможливо відтворити й пояснити.

Помилка №5: вмикати надто широкий пакет і отримувати шум замість відповіді.
Іноді здається, що пакет com.example.catalogservice — це «мій код». Так, але там можуть жити і конфігурація, і bootstrap, і багато всього. Якщо задача — зрозуміти фільтрацію каталогу, краще вмикати DEBUG рівно для com.example.catalogservice.catalog. Чим точніша зона, тим швидше ви знаходите причину й тим менше у вас спокуси «читати все підряд».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ