JavaRush /Курсы /Swift SELF /Уровни логов: debug / info / warn / error

Уровни логов: debug / info / warn / error

Swift SELF
54 уровень , 1 лекция
Открыта

1. Зачем нужны уровни и категории логов

Если вы только начинаете, кажется логичным: «ну я же и так могу писать print() везде, где мне интересно». Это действительно работает… ровно до момента, пока вы не добавили второй файл, третий модуль и первую “странную” ошибку, которая случается только у пользователя, но почему-то не у вас. Тут выясняется неприятная вещь: без правил лог быстро превращается в шум, а шум — в фон, который мозг учится игнорировать. И в этот момент вы внезапно перестаёте видеть важные события, потому что они утонули в тысяче строк “я тут был”.

Уровни и категории решают два разных класса проблем.

Уровни отвечают на вопрос «насколько это важно?», чтобы мы могли, например, выключить “болтливость” и оставить только реально критичное.

Категории отвечают на вопрос «кто именно это написал?», чтобы в потоке сообщений было видно: это парсер команд шалит, или сервис, или хранилище.

Вместе они превращают логирование в управляемый инструмент, а не в коллекцию случайных фраз.

2. Уровни логов: debug, info, warn, error

Когда говорят «уровень логирования», многие представляют себе настроение разработчика: сегодня я на debug, завтра на error, послезавтра на “аааа!”. На самом деле уровень — это договорённость о смысле события. Причём договорённость нужна не компилятору, а вам будущему (и вашему тимлиду будущему, и коллеге будущему, который откроет лог и будет судорожно искать “почему оно сломалось”). Если уровень выбран правильно, то даже короткий лог рассказывает историю: что программа делала, где насторожиться и где она действительно упала.

Важно, что уровни обычно имеют порядок: от менее важного к более важному. Мы берём практичный набор для CLI: debug, info, warn, error. Иногда в библиотеках встречаются ещё trace или critical, но для учебного CLI это будет лишний шум (иронично, да).

Ниже — удобная шпаргалка по смыслу. Старайтесь воспринимать её не как закон природы, а как стабильное соглашение, которого вы придерживаетесь во всём проекте.

Уровень Когда использовать Типичный смысл
debug
Когда вы хотите увидеть “как мы дошли до решения” Детали контроля потока, шаги алгоритма, “начали/закончили операцию”, дополнительные параметры
info
Когда событие полезно знать почти всегда Старт приложения, успешно выполненная команда, важное состояние “сделано”
warn
Когда что-то странное, но программа может продолжать Непривычный, но обрабатываемый сценарий: игнорируем неизвестный аргумент, используем дефолт, пропускаем битую строку
error
Когда операция не выполнена (или приложение не может продолжать этот сценарий) Ошибка валидации, отказ 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 started", minLevel: .info)     // напечатает
log(.debug, "tokens parsed: 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 такая:

Категория Про что Где живёт в приложении
app
вход в программу, общий жизненный цикл, запуск команды
main.swift / App.run()
parser
разбор строки команды в структуру (команда + аргументы)
CommandParser
service
бизнес-операции: добавить книгу, удалить, поиск
LibraryService
repository
хранение/доступ к данным (пока в памяти)
InMemoryRepository

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: "received input", config: config)

    log(.debug, category: .parser, message: "rawLength=\(input.count)", config: config)

    if input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
        log(.error, category: .parser, message: "empty input", config: config)
        print("Команда пуста.") // UX-сообщение пользователю
        return
    }

    log(.info, category: .service, message: "add book success", 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 используется для дополнительной диагностики и high-level контроля потока внутри операции, а более высокие уровни не должны захламляться обычной работой.

Ошибка №3: категории делаются строками “как получится”.
Проект расползается на "parser", "Parser", "cmdParser", "tokenizer". Снаружи это выглядит как мелочь, но фильтрация и поиск ломаются мгновенно. Сегодня вы ищете "parser", завтра лог внезапно пишет "Parser", и вы не видите половину событий. Категории должны быть фиксированным словарём, и enum LogCategory — простой способ заставить проект держаться одной терминологии.

Ошибка №4: warn ставится там, где ничего сделать нельзя.
Получается лог “мы обнаружили странный заголовок/аргумент/символ”, но пользователь не может это исправить, и разработчик тоже не может принять решение. Практический смысл предупреждений именно в том, что они должны быть actionable: на них можно реагировать, а не просто нервничать. Иначе предупреждения засоряют поток и перестают выполнять роль раннего сигнала.

Ошибка №5: смешивание уровней и пользовательского UX.
Например, когда вы печатаете пользователю "Ошибка: ParseError.missingTitle(line: …)" и думаете, что это полезно. Это полезно только разработчику, а пользователь видит набор странных слов и начинает бояться компьютера. Уровень и категория — это про диагностику, а пользовательский текст — про “что делать дальше”. Даже если технически вы всё выводите через print, смысл у этих сообщений разный, и их нельзя путать.

1
Задача
Swift SELF, 54 уровень, 1 лекция
Недоступна
Строка лога
Строка лога
1
Задача
Swift SELF, 54 уровень, 1 лекция
Недоступна
Фильтр уровня
Фильтр уровня
1
Задача
Swift SELF, 54 уровень, 1 лекция
Недоступна
Лента событий
Лента событий
1
Задача
Swift SELF, 54 уровень, 1 лекция
Недоступна
Сводка смены
Сводка смены
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ