Последнее обновление: 4 февраля 2026
Автор: Антон Козицкий

Создаем свой логгер с нуля

Интро

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

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

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

public class UserService {

  private static final Logger log = LoggerFactory.getLogger(UserService.class);

  public User getUser(UUID userId) {
    log.info("Getting user by id={}", userId);
    // other code
  }
}

Это выводило лог наподобие этого:

2025-02-13 09:02:17 INFO  Getting user by id=093d38ea-09fa-4a40-9f6a-ce758595ef68

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

  • Логи не выводились по непонятной для меня причине. То есть в коде они были добавлены, но после запуска программы их не было видно: ни в консоли, ни в файле для логов;
  • Для меня всегда были "магией" файлы с конфигурацией логгеров. Часто я просто брал готовый конфиг и переиспользовал, так и не поняв что там и для чего в нём;
  • Способ задания уровней логирования. Для меня это тоже было "некой" магией, когда мы в конфигурации Spring Boot, в его application.properties/application.yaml просто указываем имена пакетов и напротив выставляем желаемый уровень логирования. Написал — работает, на этом моё погружение в тему и заканчивалось;
  • Для меня всегда было загадкой то, как логи из нашего приложения оказываются в агрегаторе логов (DataDog / GrayLog / Docker container logs).

Чтобы наконец-то раскрыть для себя эту тему, я решил углубиться в тему логирования, а также попробовать самостоятельно реализовать простейший логгер (без оптимизаций — просто базовую версию). Делюсь с вами кодом здесь. Очень надеюсь, что и для вас это сыграет ту же полезную роль, что и для меня.

Простой логгер

Прежде чем мы перейдем к коду, давайте сначала обсудим важные моменты о том, как логгеры устроены и почему они устроены так, а не иначе.

Один файл = один логгер

И на это есть свои причины.

Первая причина — иметь возможность видеть в логах контекст. Посмотрите на эти логи:

2026-02-13 09:13:57.533 [restartedMain] INFO  org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean - Initialized JPA EntityManagerFactory for persistence unit 'default'

2026-02-13 09:13:57.626 [restartedMain] INFO  io.devmentor.dmweb.service.impl.CodeSnippetServiceImpl - Loading code snippets config from: file:/C:/Users/Anton/projects/dm/dm-web/target/classes/code-snippets/snippets-map.yaml

2026-02-13 09:13:57.631 [restartedMain] INFO  io.devmentor.dmweb.dao.repository.impl.YamlArticleRepository - Loading articles from: file:/C:/Users/Anton/projects/dm/dm-web/target/classes/articles.yaml

2026-02-13 09:13:57.918 [restartedMain] INFO  org.springframework.data.jpa.repository.query.QueryEnhancerFactory - Hibernate is in classpath; If applicable, HQL parser will be used.

Думаю, вы замечаете, что каждый лог ссылается на класс, из которого он был выброшен. Это очень удобно во время отладки, так как в голове у себя вы с легкостью можете состыковать лог с классом, из которого он был выброшен.

Вторая причина — возможность задавать уровни логирования гранулировано. Посмотрите на этот пример, чтобы понять что я имею в виду:

logging:
  level:
    ROOT: info
    io:
      devmentor:
        dmweb:
          controller:
            exception: warn
    org:
      hibernate: warn
      springframework:
        web: error
        web.servlet.DispatcherServlet: debug
        security: trace
      thymeleaf: info
      thymeleaf.templateresolver: debug

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

"Фабрика" для управления логгерами

Мы используем паттерн "фабрика" для получения логгеров. В своём коде мы просто вызываем фабричный метод LoggerFactory.getLogger(Our.class) и всё — всю остальную работу логгер делает "под капотом" сам. Что именно он делает, мы рассмотрим дальше.

Имплементация

Полный код проекта вы найдете на GitHub — devmentor-io/simple-logger .

Структура проекта

Ниже представлена структура нашего логгера:

src/main/java/io/devmentor/learning/
├── simplelogger/
│   ├── LogLevel.java        — enum, определяющий уровни логирования (TRACE, DEBUG, INFO, WARN, ERROR)
│   ├── Logger.java          — класс логгера
│   └── LoggerFactory.java   — фабрика логгеров
├── SimpleTestApplication.java    — пример базового использования логгера
└── AdvancedTestApplication.java  — пример расширенного использования логгера

По сути, наш проект будет состоять из трёх основных классов:

  1. ENUM - здесь мы опишем все уровни логирования, которые мы хотели бы поддерживать;
  2. Класс логгера, у которого будет имя, будет указан уровень логирования и ссылка на родительский логгер;
  3. Фабрика, которую мы будем использовать, чтобы получить логгер по имени.

Теперь давайте пройдёмся подробно по каждой части.

LogLevel

public enum LogLevel {
  TRACE,
  DEBUG,
  INFO,
  WARN,
  ERROR;

  /*
    Вызывая этот метод мы будем решать: логировать или нет. И основано это решение
    будет на "включенном" уровне логирования для конкретного лога из логгера.
    Например, мы выставили на настройках логгера для класса UserService.java что
    хотим логировать на уровне INFO. Но в коде у нас идет DEBUG-лог. В этом случае
    лог не будет показан. Но будет показан, если мы изменим в конфигурации уровень
    логирования на DEBUG или TRACE.
    То есть здесь в методе мы сравниваем "заданный уровень логирования" с
    фактическим "уровнем логирования" лога, который мы собираемся отобразить
  */
  public boolean isEnabled(LogLevel other) {
    return other.ordinal() >= this.ordinal();
  }
}

Наш логгер поддерживает пять уровней логирования. Прошу заметить, что некоторые логгеры также поддерживают уровень FATAL, но мы не будем его включать в нашу реализацию, так как используется он достаточно редко. Если и используется, то не в бизнес-логике, а в системных приложениях.

Logger

import java.time.LocalDateTime;

public class Logger {

  private final String name;
  private LogLevel level;
  private Logger parent;

  /*
    Единственным обязательным параметром для логгера у нас является его имя, а
    имя логгера - это его fully qualified name, например:
    "io.devmentor.learning.SimpleTestApplication"
   */
  public Logger(String name) {
    this.name = name;
  }

  /*
    В LoggerFactory мы будем устанавливать ссылку на родительский логгер
    для текущего логгера
   */
  public void setParent(Logger parent) {
    this.parent = parent;
  }

  /*
    Этот метод используется для задания уровня логирования для логгера.
    Если не задан в какое-то особое желаемое значение, то по умолчанию
    будет INFO (стандарт для продакшн)
   */
  public void setLevel(LogLevel level) {
    this.level = level;
  }

  public void trace(String message) {
    log(LogLevel.TRACE, message);
  }

  public void debug(String message) {
    log(LogLevel.DEBUG, message);
  }

  public void info(String message) {
    log(LogLevel.INFO, message);
  }

  public void warn(String message) {
    log(LogLevel.WARN, message);
  }

  public void error(String message) {
    log(LogLevel.ERROR, message);
  }

  /*
    Приватный метод, который вызывают все методы логирования, передавая
    желаемый уровень в параметре метода. Если уровень включен (проверяем в LogLevel#isEnabled(LogLevel other)),
    то отображаем. В противном случае - просто игнорируем (не выводим лог)
   */
  private void log(LogLevel level, String message) {
    LogLevel effectiveLevel = getEffectiveLevel();

    if (!effectiveLevel.isEnabled(level)) {
      return;
    }

    /*
      Формируем лог по заданному шаблону. Пример получившегося лога:
      2026-02-13T12:33:10.675385900 INFO  [io.devmentor.learning.SimpleTestApplication] - Calling SimpleTestApplication.main() method
      В настоящих логгерах эту конфигурацию можно задавать через файл конфигурации, подробно описывая шаблон
    */
    System.out.printf(
        "%s %-5s [%s] - %s%n",
        LocalDateTime.now(),
        level,
        name,
        message
    );
  }

  /*
    В этом методе пытаемся рекурсивно вычислить уровень логирования для текущего логгера.
    У нас есть такие варианты:
      1. текущий логгер имеет свой уровень логирования - будет использоваться он (задается через Logger#setLevel(LogLevel level) метод)
      2. уровень логирования задан в одном из родительских логгеров (найдем через рекурсивный поиск)
      3. уровень логирования не задан ни в этом логгере, ни в одном из родительских (будет использован
      уровень логирования из ROOT-логгера, который является логгером по умолчанию и всегда существует)
   */
  private LogLevel getEffectiveLevel() {
    if (level != null) {
      return level;
    }
    if (parent != null) {
      return parent.getEffectiveLevel();
    }

    return LogLevel.INFO; // default logging level
  }
}

LoggerFactory

Все логи мы храним в Java Map. А для того чтобы нашу реализацию сделать потокобезопасной, мы будем использовать имплементацию ConcurrentHashMap.

У нас всегда будет существовать ROOT-логгер, который будет родительским для всех остальных логгеров, если не были заданы более младшие "родители" для них.

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class LoggerFactory {

  private static final Map<String, Logger> LOGGERS = new ConcurrentHashMap<>();
  private static final Logger ROOT_LOGGER = new Logger("ROOT");

  /*
    Задаем уровень логирования по умолчанию для ROOT-логгера.
    Кладем ROOT-логгер в мапу с логгерами
   */
  static {
    ROOT_LOGGER.setLevel(LogLevel.INFO);
    LOGGERS.put("ROOT", ROOT_LOGGER);
  }

  /*
    Фабричный метод для получения логгера по его имени, в роли которого выступает
    fully qualified name
  */
  public static Logger getLogger(Class<?> clazz) {
    String fullyQualifiedName = clazz.getName();
    return getLogger(fullyQualifiedName);
  }

  /*
    Пробуем получить логгер по имени из мапы. Если нет такого логгера еще, то создаем
   */
  public static Logger getLogger(final String name) {
    return LOGGERS.computeIfAbsent(name, LoggerFactory::createLogger);
  }

  /*
    Создаем новый логгер, указывая его новое имя. Выглядит вот так: "io.devmentor.learning.SimpleTestApplication".
    Затем находим родительский логгер, рекурсивно проходя по частям его имени
   */
  private static Logger createLogger(String name) {
    Logger logger = new Logger(name);

    Logger parent = findParentLogger(name);
    logger.setParent(parent);

    return logger;
  }

  /*
    Идентификатором логгера служит его fully qualified name,
    которое состоит из частей, разделенных точками. Рекурсивно проходим и
    ищем родительский лог:

    "io.devmentor.learning.SimpleTestApplication"
    "io.devmentor.learning"
    "io.devmentor"
    "io"
    ROOT
   */
  private static Logger findParentLogger(String name) {
    int lastDot = name.lastIndexOf('.');

    // если "SimpleTestApplication", то вернет -1
    while (lastDot > 0) {
      String parentName = name.substring(0, lastDot);
      Logger parent = LOGGERS.get(parentName);

      if (parent != null) {
        return parent;
      }

      lastDot = parentName.lastIndexOf('.');
    }

    return ROOT_LOGGER;
  }
}

Как работать с этим кодом

У вас есть несколько опций:

  • Посмотреть на код здесь и все на этом — лёгкое погружение;
  • Склонировать проект с GitHub, открыть в IntelliJ IDEA, попробовать запустить, посмотреть на результат;
  • Можете к опции номер два добавить ещё отладку: поставить брекпоинты и посмотреть как код работает — это даст вам более глубокое представление;
  • Попробовать реализовать логгер по памяти, не подсматривая сюда — самый эффективный способ обучения.

Выбирайте свой способ из предложенных выше, в зависимости от ваших целей и от важности этой темы для вас.

А я с вами не прощаюсь и мы увидимся в следующей статье, в которой поговорим про инструменты логирования - готовые решения, которые мы можем использовать в своих проектах.