1. Навіщо потрібні рівні та категорії логів
Якщо ви тільки починаєте, може здаватися логічним: «Ну я ж і так можу всюди писати print()». Це справді працює… рівно до того моменту, поки ви не додали другий файл, третій модуль і першу «дивну» помилку, яка трапляється лише в користувача, але чомусь не у вас. Тут виявляється неприємна річ: без правил лог швидко перетворюється на шум, а шум — на фон, який мозок вчиться ігнорувати. І тоді ви раптово перестаєте бачити важливі події, бо вони втонули в тисячах рядків «я тут був».
Рівні та категорії розв’язують два різні типи проблем.
Рівні відповідають на запитання «наскільки це важливо?», щоб ми могли, наприклад, вимкнути «балакучість» і залишити лише справді критичне.
Категорії відповідають на запитання «хто саме це написав?», щоб у потоці повідомлень було видно, що саме дало збій: парсер команд, сервіс чи сховище.
Разом вони перетворюють логування на керований інструмент, а не на набір випадкових фраз.
2. Рівні логів: debug, info, warn, error
Коли кажуть «рівень логування», багато хто уявляє собі настрій розробника: сьогодні я на debug, завтра на error, післязавтра на «аааа!». Насправді рівень — це домовленість про зміст події. Причому ця домовленість потрібна не компілятору, а вам у майбутньому, вашому тімліду в майбутньому і колезі, який відкриє лог та судомно шукатиме, чому все зламалося. Якщо рівень вибрано правильно, навіть короткий лог розповідає історію: що застосунок робив, де варто насторожитися і де він справді впав.
Важливо, що рівні зазвичай мають порядок: від менш важливого до більш важливого. Ми беремо практичний набір для CLI: debug, info, warn, error. Іноді в бібліотеках трапляються ще trace або critical, але для навчального CLI це буде зайвий шум — іронічно, так.
Нижче — зручна шпаргалка за змістом. Сприймайте її не як закон природи, а як стале правило, якого ви дотримуєтеся в усьому проєкті.
| Рівень | Коли використовувати | Типовий зміст |
|---|---|---|
|
Коли ви хочете побачити «як ми дійшли до рішення» | Деталі керування потоком, кроки алгоритму, «почали/завершили операцію», додаткові параметри |
|
Коли подію корисно знати майже завжди | Старт застосунку, успішно виконана команда, важливий стан «зроблено» |
|
Коли щось дивне, але застосунок може продовжувати роботу | Незвичний, але оброблюваний сценарій: ігноруємо невідомий аргумент, використовуємо значення за замовчуванням, пропускаємо битий рядок |
|
Коли операцію не виконано або застосунок не може продовжити цей сценарій | Помилка валідації, збій I/O, внутрішня помилка в логіці (але внутрішнє — обережно) |
Є корисна думка з практик екосистеми Swift Server: debug зазвичай використовують для верхньорівневого трасування того, як пройшла операція (почали → кроки → завершили), і для парних подій «begin/end». Там же наголошують, що надто «важкі» рівні в бібліотеках, наприклад логування error на будь-який мережевий збій, можуть бути шкідливими, бо код, який викликає, сам вирішує, що вважати помилкою і як це повідомляти.
У нашому CLI ми не бібліотека, але принцип той самий: рівень має відображати саме важливість, а не «мені страшно».
3. Шкала рівнів і фільтрація за minLevel
Якщо рівні не впорядковані, фільтрація перетворюється на магію з рядками: «показувати усе, крім debug, але warn показувати, а error завжди, а ще…» — і ось у вас уже міні-мова програмування всередині if. Набагато простіше зробити рівні числовою шкалою: debug — най«тихіший», error — най«гучніший». Тоді фільтр звучить просто: «показуємо усе, що не нижче за мінімальний рівень».
Зробимо enum з Int-значеннями. Це коротко, зрозуміло, і компілятор допомагає не переплутати рядки.
enum LogLevel: Int {
case debug = 0
case info = 1
case warn = 2
case error = 3
}
Тепер фільтр перетворюється на елементарне порівняння:
enum LogLevel: Int { case debug = 0, info = 1, warn = 2, error = 3 }
func shouldLog(_ level: LogLevel, minLevel: LogLevel) -> Bool {
level.rawValue >= minLevel.rawValue
}
print(shouldLog(.debug, minLevel: .info)) // false
print(shouldLog(.error, minLevel: .info)) // true
Зверніть увагу на маленький, але важливий сенс: minLevel = .info означає «debug вимкнено, але усе, починаючи з info, увімкнено». Це типове налаштування для звичайного запуску, коли ви не налагоджуєте кожну гілку, але хочете бачити загальну картину.
4. Приклад рівнів у LibraryCLI
У нашому навчальному застосунку LibraryCLI — умовно консольній бібліотеці книжок — події відбуваються в різних місцях. Спочатку застосунок стартує й читає команду користувача, потім парсер намагається розібрати введення, далі сервіс виконує дію, а потім, поки що, ми можемо зберігати дані в памʼяті. Навіть із простим сховищем уже зʼявляється вибір: що логувати як info, а що як debug, і де доречний warn.
Уявіть сценарій: користувач вводить команду add "Swift для людей". Під час звичайного запуску нам важливо бачити, що команду виконано, але зовсім не обовʼязково показувати внутрішні деталі токенізації. Отже, повідомлення «книгу додано» — це радше info, а «розібрано N токенів» — debug.
Зробимо дуже просту функцію логера, поки що без протоколу — його спроєктуємо окремо в іншій лекції. Тут наша мета — не архітектура логера, а саме рівні та категорії, тож реалізація буде навмисно примітивною.
enum LogLevel: Int { case debug = 0, info = 1, warn = 2, error = 3 }
func log(_ level: LogLevel, _ message: String, minLevel: LogLevel) {
guard level.rawValue >= minLevel.rawValue else { return }
print("[\(level)] \(message)")
}
log(.info, "LibraryCLI запущено", minLevel: .info) // надрукує
log(.debug, "розібрано токенів: 3", minLevel: .info) // не надрукує
Так, формат поки що кумедний ("[\(level)]" виведе info як info). Ми свідомо поки не переходимо до гарного форматування: це вже окремий пласт, і він буде. Тут головне — відчути, що рівень керує видимістю події.
Тепер про warn. Припустімо, користувач написав add без назви. Це не кінець світу для застосунку, але конкретну операцію не виконано. Користувачеві ми покажемо дружнє повідомлення — «бракує аргументів», а в лог запишемо технічнішу деталь: яка команда, які аргументи надійшли. За рівнем це найчастіше error, бо операцію не виконано.
Якщо ж ситуація така: ввід дивний, але ми можемо продовжити зі значенням за замовчуванням, тоді warn логічніший. Наприклад, користувач передав невідомий прапорець, ми його ігноруємо й продовжуємо. Це «підозріло», але не провал.
5. Категорії логів: хто пише повідомлення
Коли проєкт маленький, рядок лога «не вдалося розібрати» виглядає нормально. Коли проєкт виростає, зʼявляється запитання: «А де саме не вдалося?» — у парсері команди, у парсері дати, у доменній валідації чи в сховищі? Без категорії ви починаєте додавати слова в текст: «парсер команди: не вдалося…», «сервіс: не вдалося…», «репозиторій: не вдалося…». По-перше, це копіпаста. По-друге, це неминуче розмивається: один написав «parser», інший — «Parser», третій — «парсер», четвертий — «tokenizer», і фільтрувати це неможливо.
Категорія — це фіксований тег джерела. Найпростіший і дисциплінувальний спосіб — enum з рядковим rawValue.
enum LogCategory: String {
case app
case parser
case service
case repository
}
print(LogCategory.parser.rawValue) // parser
Категорія — не заміна рівню. Це інша вісь. Уявіть координати: рівень — «наскільки важливо», категорія — «де сталося». Одна й та сама подія «операцію не виконано» може бути error і в категорії .parser, і в категорії .repository — але це будуть різні за змістом помилки.
Зручна ментальна модель для LibraryCLI така:
| Категорія | Про що | Де це в застосунку |
|---|---|---|
|
вхід у застосунок, загальний життєвий цикл, запуск команди | |
|
розбір рядка команди у структуру (команда + аргументи) | |
|
бізнес-операції: додати книжку, видалити, пошук | |
|
зберігання даних / доступ до них (поки що в памʼяті) | |
6. Фільтрація за категоріями та практичні нюанси
Фільтр за рівнем — це «рубильник гучності». Але інколи вам потрібно не голосніше чи тихіше, а прибрати барабани й залишити гітару. Наприклад, ви налагоджуєте парсер і хочете бачити .parser на debug, але вам зовсім нецікаві подробиці репозиторію. Або навпаки: ви лагодите зберігання і хочете бачити .repository, але не хочете читати кожну спробу токенізації.
Зробімо конфігурацію, яка містить і мінімальний рівень, і список увімкнених категорій. Для списку категорій зручно використовувати Set, бо перевірка contains виглядає акуратно, а семантика «увімкнено/вимкнено» читається природно.
struct LogConfig {
let minLevel: LogLevel
let enabledCategories: Set<LogCategory>
}
func shouldLog(_ level: LogLevel, category: LogCategory, config: LogConfig) -> Bool {
level.rawValue >= config.minLevel.rawValue && config.enabledCategories.contains(category)
}
Тепер наша функція log може виглядати так:
func log(_ level: LogLevel, category: LogCategory, message: String, config: LogConfig) {
guard shouldLog(level, category: category, config: config) else { return }
print("[\(category.rawValue)] [\(level)] \(message)")
}
І приклад використання:
let config = LogConfig(minLevel: .debug, enabledCategories: [.app, .parser])
log(.info, category: .app, message: "start", config: config) // виведеться
log(.debug, category: .parser, message: "tokens=4", config: config) // виведеться
log(.debug, category: .repository, message: "save called", config: config) // не виведеться
Суть цього підходу не в красі print, а в керованості. Ви не лізете в код і не коментуєте print(). Ви змінюєте конфігурацію й отримуєте потрібний зріз подій.
Мінісценарій: один ввід на різних рівнях і категоріях
Тепер зберемо маленький фрагмент історії для команди add. Це не повна архітектура, а лише демонстрація того, як рівні та категорії допомагають читати те, що відбувається.
Уявімо, що в нас є функція, яка імітує обробку команди. Будь ласка, не сприймайте цей код як кінцевий дизайн — він навчальний і навмисно короткий.
func handleAddCommand(input: String, config: LogConfig) {
log(.info, category: .app, message: "отримано введення", config: config)
log(.debug, category: .parser, message: "rawLength=\(input.count)", config: config)
if input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
log(.error, category: .parser, message: "порожнє введення", config: config)
print("Команда порожня.") // UX-повідомлення користувачеві
return
}
log(.info, category: .service, message: "успішне додавання книги", config: config)
print("Книгу додано.") // UX-повідомлення користувачеві
}
Якщо minLevel = .info і всі категорії увімкнено, користувач побачить приблизно таку картину: «отримано введення», «успішно додано». Якщо minLevel = .debug, додасться технічна деталь щодо довжини рядка, і під час розслідування проблеми ви зрозумієте, що саме надійшло до вас. Якщо вимкнути категорію .service, сервісні повідомлення зникнуть, але застосунок продовжить працювати.
Окремо зауважте важливий момент дисципліни: ми не підміняємо обробку помилок логуванням. Якщо введення порожнє, ми виходимо, і користувач бачить зрозуміле повідомлення. Лог — це «слід для розробника», а не контракт роботи програми.
Чому не варто логувати усе як error
На емоціях дуже хочеться: «Якщо щось не так — error!». Але тоді ви втрачаєте здатність відрізняти справді критичне від просто незвичного. У продакшені, навіть у маленькому CLI, це призводить до того, що на кожну дрібницю у вас «помилка», і ви перестаєте реагувати. Це якби пожежна сигналізація верещала щоразу, коли ви підсмажили тост.
У практиках серверного Swift є хороша думка: не варто використовувати warning/error для подій, на які користувач системи ніяк не може вплинути або які є очікуваними в деяких сценаріях. Такі логи створюють «засмічення» і плутають діагностику. У нашому CLI цю думку можна сформулювати так: warn має бути про «є привід насторожитися та/або виправити введення чи налаштування», а error — про «операцію не виконано».
7. Типові помилки під час вибору рівнів і категорій
Помилка № 1: усе логують як .error, бо «так точно не пропустимо».
Це перетворює лог на нескінченний крик «вовки!». За тиждень ви вже не відрізняєте «не знайдено аргумент команди» від «застосунок не може працювати взагалі». У результаті ви або ігноруєте лог, або починаєте писати навколо нього додаткові пояснення, які мали б бути виражені рівнем. Правильніше вважати error рівнем відмови операції, а warn — рівнем «неідеально, але працюємо», і залишати info для нормальних важливих етапів.
Помилка № 2: плутанина debug і info.
У info записують кожну дрібницю, і при будь-якому запуску ви отримуєте стіну тексту. А потім, коли вам справді знадобляться деталі, ви ввімкнете debug — і побачите другу стіну тексту. У хороших практиках debug використовують для додаткової діагностики та верхньорівневого контролю потоку всередині операції, а вищі рівні не повинні засмічуватися буденною роботою.
Помилка № 3: категорії робляться рядками «як вийде».
Проєкт розповзається на "parser", "Parser", "cmdParser", "tokenizer". Ззовні це виглядає як дрібниця, але фільтрація й пошук ламаються миттєво. Сьогодні ви шукаєте "parser", завтра лог раптом пише "Parser", і ви не бачите половини подій. Категорії мають бути фіксованим словником, і enum LogCategory — простий спосіб змусити проєкт дотримуватися однієї термінології.
Помилка № 4: warn ставлять там, де нічого зробити не можна.
Виходить лог «ми виявили дивний заголовок/аргумент/символ», але користувач не може це виправити, і розробник теж не може ухвалити рішення. Практичний зміст попереджень саме в тому, що на них можна реагувати, а не просто нервувати. Інакше попередження засмічують потік і перестають виконувати роль раннього сигналу.
Помилка № 5: змішування рівнів і користувацького UX.
Наприклад, коли ви друкуєте користувачеві "Помилка: ParseError.missingTitle(line: …)" і думаєте, що це корисно. Це корисно лише розробникові, а користувач бачить набір дивних слів і починає боятися компʼютера. Рівень і категорія — це про діагностику, а користувацький текст — про «що робити далі». Навіть якщо технічно ви усе виводите через print, зміст цих повідомлень різний, і їх не можна плутати.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ