Логирование ошибок java

Время на прочтение
7 мин

Количество просмотров 233K

В ходе моей работы в компании DataArt я, в числе прочего, занимаюсь менторской деятельностью. В частности это включает в себя проверку учебных заданий сделанных практикантами. В последнее время в заданиях наметилась тенденция «странного» использования логеров. Мы с коллегами решили включить в текст задания ссылку на статью с описанием java logging best practices, но оказалось, что такой статьи в которой бы просто и без лишних деталей на практике объяснялось бы как надо писать в лог на Java, вот так вот с ходу не находится.

Данная статья не содержит каких-то откровений, в ней не рассматриваются тонкости какого либо из многочисленных java logging frameworks. Здесь рассказываю как записать в лог так, чтобы это не вызвало удивления у Ваших коллег, основная цель написания включить ее в список обязательного чтения для практикантов. Если все еще интересно, читайте дальше

Несколько разъяснений.

  • Весь код примеров использует java.util.logging framework. Вопрос «Какой из фреймворков логирования ниболее кошерен» я оставлю за кадром. Скажу только что до java.util.logging проще всего дотянуться ибо он уже идет вместе с JRE и на самом деле рассказанное в данной статье с минимальными косметическими правками верно для подавляющего большинства систем логирования.
  • В целом рецепты приведенные в данной статье не являются единственно верными, есть моменты о которых можно поспорить, но в целом эти рецепты используются многие годы, многими разработчиками, во многих проектах и они достаточно хороши чтобы им следовать если у Вас нет каких-то совсем уже серьезных возражений.
  • В статье не рассматриваются такие «продвинутые» топики как:
    • Конфигурирование уровней для отдельных логеров
    • Форматирования логов
    • Асинхронное логирование
    • Создание собственных уровней логирования в Log4J
    • Контекстное логирование
    • И многое другое
  • Слово logging я пишу по русски как логирование с одной буквой «г» в основном потом, что такой вариант перевода чаще встречается
  • Советы, что, с каким уровнем логировать я пожалуй тоже оставлю за кадром т.к. тут все сильно зависит от приложения, условий эксплуатации, отношений с заказчиком и т.п. тонких вещей.
Пример №1
Хорошо
public class SomeClass {
	    
	    private static Logger log = Logger.getLogger(SomeClass.class.getName());
	    
	    public void someMethod()
	    {
	        log.info("Some message");
	    }      
...
  1. Логер это статическое поле класса инициализируемое при загрузке класса, имеет простое, короткое имя, важно чтобы во всех Ваших классах переменная логера называлась одинаково (это диктуется общим правилом, одинаковые вещи в программе должны делаться одинаковым образом).
  2. В качестве имени логера я использую имя класса, на самом деле это не единственный способ, можно пытаться организовать какую-то свою иерархию логирования (например transport layer/app layer для подсистем имеющих дело с обменом данными), но как показывает практика выдумывать и главное потом неукоснительно следовать такой иерархии крайне сложно, а вариант с именами логеров совпадающими с именами классов весьма хорош и используется в 99% проектов
  3. Здесь для записи в лог я использую короткий метод .info, а не более общий метод .log, так много лаконичнее
  4. Имя логера берется как SomeClass.class.getName(), а не как «com.dataart.demo.java.logging.SomeClass», оба способа по идее одинаковы, но первый защищает Вас от сюрпризов при рефакторинге имени/пакета класса
Плохо

public class SomeClass {
	    public void someMethod()
	    {
	         Logger.getLogger("com.dataart.demo.java.logging.SomeClass").log(Level.INFO,"Some message");
	    }    
...  

По сути тоже самое но букв больше и читается не так легко.

Замечание между примерами

Вы наверное обратили внимание, что все сообщения в примерах на английском языке. Это не случайно. Дело в том, что даже если все-все кто работает и будет работать с Вашим кодом говорят по русски, есть вероятность, что Вам придется просматривать лог сообщения на удаленном компьютере например через ssh при этом в большом количестве случаев Вы увидите примерно такое сообщение «????, ???? ?????!!!!» (я безусловно знаю что через ssh можно протащить русские буквы, но вот почему-то далеко не всегда все оказывается настроенным должным образом).
Или даже на локальной машине в cmd вы можете увидеть что вот такое:
INFO: ╨Ъ╨░╨║╨╛╨╡-╤В╨╛ ╤Б╨╛╨╛╨▒╤Й╨╡╨╜╨╕╨╡ ╨▓ ╨╗╨╛╨│

С этим безусловно тоже можно бороться. Но не всегда легко объяснить заказчику на том конце телефонной трубки, как сделать так чтобы вместо крякозябр были видны русские буквы.
Совет: Пишите лог сообщения на английском языке, ну или в крайнем случае латинскими буквами.

Пример №2

Хорошо

	       try {
	            throw new Exception("Some exception");
	        } catch (Exception ex) {
	            log.log(Level.SEVERE, "Exception: ", ex);
	        }
	        //В стандартной лог конфигурации вы это сообщение не увидите
	        log.fine("some minor, debug message");

	        /*
	          Иногда вывод лог сообщений требует достаточно больших ресурсов (например
	          дамп какого-то пакета данных и т.п.).
	          В таких случаях стоит проверить выведется ли в лог сообщение для этого уровня
	          логирования
	        */
	        if (log.isLoggable(Level.FINE)) {
	            log.fine("Some CPU consuming message: " + prepareCPUConsumingLogMessage());
	        }
  1. Если Вам необходимо залогировать исключение, для этого служит метод .log(level,message,exception)
  2. Если вы специально не настроили конфигурацию лог системы, сообщения с уровнем ниже info, например fine выводиться не будут. Но писать их по крайней мере для важных частей системы стоит. Когда что-то пойдет не так, Вы настроите более подробный уровень логирования и увидите много интересного.
  3. Слишком много лог сообщений, даже если они физически не пишутся в лог файл из-за своего слишком маленького уровня, могут существенно замедлить выполнение программы. Особенно если для подготовки самого сообщения надо потратить много ресурсов. Для этого есть метод .isLoggable(level) — он позволяет узнать пропустит ли текущая конфигурация логера данное сообщение
Плохо


	       try {
	            throw new Exception("Some exception");
	        } catch (Exception ex) {
	            log.severe("Exception: " + ex.toString() );
	        }
                log.fine("Some CPU consuming message: " + itTakes500MillisecondsToPrepageThisMessage());
	

Если логировать только ex.toString(), то потом Вы не сможете понять в какой строке изначально сработало исключение.

Пример №3

Логер надо конфигурировать. Есть конфигурация по умолчанию она выводит в консоль все сообщения с уровнем INFO и выше. Она достаточно хороша, для разработки из IDE, но для реального приложения ее обычно неплохо бы подправить.

Какие тут есть варианты

По умолчанию: Файл logging.properties для уровня INFO, вывод в консоль

#Console handler
handlers= java.util.logging.ConsoleHandler
.level=INFO

Делаем логирование более подробным выводим еще и сообщения уровня FINE

#Console handler
handlers= java.util.logging.ConsoleHandler
.level=FINE
java.util.logging.ConsoleHandler.level = FINE

Что мы тут сделали

  • Установили уровень FINE для корневого логера, просто чтобы сообщения пролезали внутрь лог системы.
  • И сказали что все что пролезет через лог систему надо выводить на консоль от уровня FINE и выше.
Выводим лог сообщения куда-то еще

Чем плох вывод на консоль? Консоль это по сути дела старый добрый stderr. Что это значит:

  • Если приложение запускается с помощью javaw Вы вообще ничего не увидите.
  • Если вывод идет в консоль и нужное вам сообщение промелькнуло 4 часа назад буфер консоли его уже съел, информация пропала.
  • Если вывод консоли направлен в файл java com.yourcompanyname.EntryClass 2>>application_log.txt и приложение работает не останавливаясь несколько недель — файл будет весьма и весьма большим, рискуя занять весь диск.

Чтобы решить эти проблемы был придуман java.util.logging.FileHandler — хэндлер который выводит лог сообщения в файл. При этом он умеет ротировать файлы, т.е. после достижения максимально допустимого размера, он дописывает в файл текщуее лог сообщение и открывает новый файл с инкрементальным префиксом. И так по кругу. Например

handlers= java.util.logging.FileHandler
java.util.logging.FileHandler.pattern = application_log.txt
java.util.logging.FileHandler.limit = 50
java.util.logging.FileHandler.count = 7
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

создаст вот такие файлы (последняя колонка — размер в байтах)

application_log.txt.0                                                │ 0
application_log.txt.1                                                │ 79
application_log.txt.2                                                │ 79
application_log.txt.3                                                │ 676
application_log.txt.4                                                │ 87
application_log.txt.5                                                │ 114

Мы указали максимальный размер 50 байтов, в реальной жизни надо скорее указывать не меньше мегабайта, например вот так (я знаю, что 1000000 это чуть меньше мегабайта, но кому охота по памяти писать 1048576, если суть дела это фактически не меняет)

java.util.logging.FileHandler.limit = 1000000

В примере, как мы видим, файлы получились больше 50 байт потому что размер по сути округляется вверх до последнего целого лог сообщения. Т.е. если Вы укажете размер 1 байт и запишете лог сообщение размером в 1000 байт то размер файла станет 1000 байт и после этого лог сообщения файл закроется и откроется следующий.

copy & paste конфиг для реальной жизни, его вполне хватает для большинства service, console и desktop приложений.

handlers= java.util.logging.FileHandler

java.util.logging.FileHandler.pattern = application_log.txt
java.util.logging.FileHandler.limit = 1000000
java.util.logging.FileHandler.count = 5
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
Последняя часть магии

Ну и последнее о чем осталось рассказать — как собственно сконфигурировать логер из файла свойств. Есть два способа:

  1. Из командной строки запуска приложения
  2. В первых строчках кода Вашего приложения

Первый чуть более правильный ибо он декларативный и работает сразу, до того как начал работать код Вашего приложения.

Вот так

java Djava.util.logging.config.file=logging.properties com.dataart.application.ClassName

Но к сожалению менять строку запуска не всегда можно или не всегда удобно. Второй способ тоже неплохо работает.


    public static void main(String[] args) {
        try {
            LogManager.getLogManager().readConfiguration(
                    MainApplicationEntryClass.class.getResourceAsStream("/logging.properties"));
        } catch (IOException e) {
            System.err.println("Could not setup logger configuration: " + e.toString());
        }
        .....
  • Здесь MainApplicationEntryClass — это класс — точка входа в Ваше приложение, видимо имя класса у Вас будет другое
  • Сам файл logging.properties как правило в таких случаях кладется в корень иерархии классов и выглядит это например вот так

Что осталось за кадром

В реальной жизни как минимум половина всех Java приложений это web приложения. Сама техничка логирования в них совершенно не отличается от изложенного выше. Ну может быть за тем исключением что разные сервера приложений могут использовать разные библиотеки логирования такие например как:

  • Log4J
  • JULI logger (строго говоря это не вполне самостоятельный фреймворк, а своего рода надстройка над java.util.logging)
  • SLF4J
  • Commons Logging

Соответственно несколько отличается настройка и имена методов. Но сам принцип меняется мало. Конкретные особенности как правило хорошо описаны в документации на сам сервер приложений, например

  • Tomcat
  • JBoss
  • Resin

Something that I do is to have a static method that handles all exceptions and I add the log to a JOptionPane to show it to the user, but you could write the result to a file in FileWriter wraped in a BufeeredWriter.
For the main static method, to catch the Uncaught Exceptions I do:

SwingUtilities.invokeLater( new Runnable() {
    @Override
    public void run() {
        //Initializations...
    }
});


Thread.setDefaultUncaughtExceptionHandler( 
    new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException( Thread t, Throwable ex ) {
            handleExceptions( ex, true );
        }
    }
);

And as for the method:

public static void handleExceptions( Throwable ex, boolean shutDown ) {
    JOptionPane.showMessageDialog( null,
        "A CRITICAL ERROR APPENED!n",
        "SYSTEM FAIL",
        JOptionPane.ERROR_MESSAGE );

    StringBuilder sb = new StringBuilder(ex.toString());
    for (StackTraceElement ste : ex.getStackTrace()) {
        sb.append("ntat ").append(ste);
    }


    while( (ex = ex.getCause()) != null ) {
        sb.append("n");
        for (StackTraceElement ste : ex.getStackTrace()) {
            sb.append("ntat ").append(ste);
        }
    }

    String trace = sb.toString();

    JOptionPane.showMessageDialog( null,
        "PLEASE SEND ME THIS ERROR SO THAT I CAN FIX IT. nn" + trace,
        "SYSTEM FAIL",
        JOptionPane.ERROR_MESSAGE);

    if( shutDown ) {
        Runtime.getRuntime().exit( 0 );
    }
}

In you case, instead of «screaming» to the user, you could write a log like I told you before:

String trace = sb.toString();

File file = new File("mylog.txt");
FileWriter myFileWriter = null;
BufferedWriter myBufferedWriter = null;

try {
    //with FileWriter(File file, boolean append) you can writer to 
    //the end of the file
    myFileWriter = new FileWriter( file, true );
    myBufferedWriter = new BufferedWriter( myFileWriter );

    myBufferedWriter.write( trace );
}
catch ( IOException ex1 ) {
    //Do as you want. Do you want to use recursive to handle 
    //this exception? I don't advise that. Trust me...
}
finally {
    try {
        myBufferedWriter.close();
    }
    catch ( IOException ex1 ) {
        //Idem...
    }

    try {
        myFileWriter.close();
    }
    catch ( IOException ex1 ) {
        //Idem...
    }
}

I hope I have helped.

Have a nice day. :)

Логирование в Java

Введение

Логирование(logging) — это процесс записи информации о событиях, происходящих в рамках какого-либо процесса с некоторым объектом. Запись может проводиться в файл регистрации или в базу данных.

Слово log, на самом деле, имеет множество значений, например протокол или журнал.
Однако обычно все говорят именно лог и производные от него: логирование и логировать.

Хотя правильнее было бы говорить наверное журналирование/протоколирование и вести журнал/протокол соответственно.

Но так никто никогда не говорит, конечно ¯(ツ)

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

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

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

Поэтому «логи всякие нужны, логи всякие важны».

Пришла пора сформулировать требования — чего вообще хотелось бы от логера.

Требования

Еще одним важным вопросом на который надо ответить — это что логировать.

Работа приложения — это происходящие в нем события, которые в свою очередь могут быть классифицированы на:

  • события, связанные с бизнес-логикой
  • события, связанные с безопасностью приложения
  • и системные события, связанные с уже конкретикой реализации — вызовов ОС, использования библиотек, фреймворков и т.д.

Все ли нужно логировать?

Если вы вдруг залогируете в общий файл-лога пароль и логин пользователя, то никто такому рад не будет, что подводит нас к мысли, что логировать надо тоже с умом.

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

Добавим сюда еще и то, что логирование — это тоже работа, а значит процессорное время. И чем больше логов мы пишем — тем больше процессорного времени тратится на это, а в ситуации, когда все работает хорошо такое чрезмерное логирование только тормозит.

С другой стороны, при возникновении проблем, особенно спорадических ошибок, чем больше у вас информации — тем лучше.

Т.е возникает требование управления информацией, которая нам нужна в данный момент, а также форматом ее вывода.

При этом, логично, что изменения этого формата и того, что мы хотим видеть в логе не должны требовать перекомпиляции всего проекта или изменения кода.

Уровни логирования

Уровень логирования — это разделение событий по приоритетам, по степени важности. Например, error — пишем ошибки, debug — пишем более подробно в лог и т.д.

Уровень логирования Описание
ALL Все сообщения
TRACE Сообщение для более точной отладки
DEBUG Дебаг-сообщение, для отладки
INFO Обычное сообщение
WARN Предупреждение, не фатально, но что-то не идеально
ERROR Ошибка
FATAL Фатальная ошибка, дело совсем плохо
OFF Без сообщения

Если проиллюстрировать это:

Уровни логирования

Принципы и понятия

В основе большинства библиотек логирования лежат три понятия: logger, appender и layout.

Это наиболее распространенные и устоявшиеся понятия, можно сказать, что это — стандарт.

Logger

Логер — это объект, область ответственности которого — вывод данных в лог и управление уровнем (детализацией) этого вывода.

Логер создается с помощью фабрики и на этапе создания ему присваивается имя. Имя может быть любым, но по стандарту имя должно быть сопряжено с именем класса, в котором вы собираетесь что-то логировать:

Logger logger = LoggerFactory.getLogger(SomeClass.class.getName());

Это дает нам имя логера в виде: ru.aarexer.example.SomeClass.

Почему так рекомендуется делать?

Потому что важным свойством логгеров является то, что они организованы иерархично. Каждый логгер имеет имя, описывающее иерархию, к которой он принадлежит. Разделитель – точка. Принцип полностью аналогичен формированию имени пакета в Java.

Получается выстраивается следующая иерархия логеров:

root <- ru <- aarexer <- example <- SomeClass

И каждому логеру можно выставить свой уровень. Установленный логгеру уровень вывода распространяется на все его дочерние логгеры, для которых явно не выставлен уровень.

При этом во главе иерархии логеров всегда стоит некотрый дефолтный рутовый(корневой) логер.

Поэтому у всех логеров будет уровень логирования, даже если явно мы не прописали для ru.aarexer.example.SomeClass его, то он унаследуется от рутового.


Вопрос:

Мы установили рутовый уровень в INFO, а ru.aarexer в DEBUG, остальным в иерархии уровень не назначен, т.е:

Логер Назначенный уровень
root INFO
ru Не назначен
ru.aarexer DEBUG
ru.aarexer.example Не назначен

Какой у какого логера будет уровень логирования?

Ответ:

Вспоминаем, что, если уровень логирования не назначен для логера, то он унаследует его от родительского, смотрим на иерархию:

root <- ru <- aarexer <- example

И получаем ответ:

Logger Назначенный уровень Уровень, который будет
root Все сообщения INFO
ru Не назначен INFO
ru.aarexer DEBUG DEBUG
ru.aarexer.example Не назначен DEBUG

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

Задача логера одна — это вызывать событие, которое приведет к логированию.

logger.info("Application started");
logger.debug("Or not");

Это событие по сути состоит из двух полей:

message = "Application started"
level = Level.Info

Appender

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

Здесь нас никто не ограничивает — можно написать свой аппендер, который пишет сообщения куда-угодно.

Получается у нас есть две точки, первая — это логгер, это начало пути, вторая — аппендер, это уже конечная точка.

Логеры и аппендеры связаны в отношении many-to-many.

У одного логгера может быть несколько аппендеров, а к одному аппендеру может быть привязано несколько логгеров.

Логеры при этому наследуют от родительских не только уровни логирования, но и аппендеры.

Например, если к root-логгеру привязан аппендер A1, а к логгеру ru.aarexer – A2, то вывод в логгер ru.aarexer попадет в A2 и A1, а вывод в ru – только в A1.


Вопрос:

Пусть у нас есть несколько аппендеров и логеров

Logger Appender
root А1
ru.aarexer А2
ru.aarexer.example.SomeClass А3

В какой аппендер попадет лог-сообщение:

LoggerFactory.getLogger(SomeClass.class.getName()).info("hello");

Ответ:

У логеров есть такое свойство как additivity. По умолчанию она установлена в true.

Это говорит о том, что логер-наследник будет свои события передавать логеру-родителю.

Смотрим на иерархию:

root <- ru <- aarexer <- example

Из всего вышесказанного делаем вывод, что событие «hello» с уровнем Level.INFO попадет во все три аппендера.


Но такое наследование аппендеров можно отключить через конфигурацию, для этого стоит посмотреть в сторону выставления флага additivity="false" на логгерах.

Layout

Layout — это формат вывода данных.

Т.е как лог-сообщения будут отформативарованы, соответственно тут у каждой библиотеки свой набор доступных форматов.

Теперь пришла пора посмотреть — что вообще есть в Java.

Библиотеки логирования в Java

Ну и самым первым логером, который можно представить себе, был и есть System.out.println и System.err.println. При этом надо помнить, что err и out — это два разных потока вывода, где err вывод не буферизуется и работает быстрее, чем out.

По сути, такой логер может писать либо info-сообщения, либо error.

Однако он не отвечает всем тем требованиям, которые мы сформулировали выше, поэтому рассмотрим альтернативы.

Наиболее популярные библиотеки логирования в Java:

  • java.util.logging или JUL, является частью JDK.
  • Apache log4j
  • logback, разработанная создателями log4j
  • Apache log4j2, продолжение log4j

Это все по сути реализации логеров в Java, ну а мы бы не писали на Java, если бы не попытались сделать нечто объединяющее, чтобы иметь возможность подменять реализации, не изменяя свой код. Некоторый адаптер.

И поэтому появились еще две библиотеки:

  • Apache Commons LoggingJCL
  • Simple Logging Facade for JavaSLF4J

Apache log4j

Это самая первая библиотека логирования, появилась еще в 1999 году.

Конфигурируется через xml, либо через properties.

Поддерживает большое количество способов вывода логов: от консоли и файла до записи в БД.

Также имеет поддержку обширного формата логирования: от обычного текстового вывода до html.

Именно он ввел понятие appender — кто пишет в лог, layout — форматирование.
В конфигурации задаются эти самые appender-ы и какого уровня сообщения попадают к какому appender-у.

Ввел соответствие иерархичности категорий и пакетов: например, можно логгировать все сообщения из org.hibernate и заглушить всё из org.hibernate.type.

Благодаря подобной иерархии лишнее отсекается и поэтому логер работает быстро.

Отсеки лишнее и не думай

Проект сейчас не развивается и по сути заброшен, с версией Java 9 уже не совместим.

Поэтому на данный момент рекомендуется использовать log4j2, о котором речь пойдет ниже.

Вклад log4j в мир логирования настолько велик, что многие идеи были взяты в библиотеки логирования для других языков.

JUL

Зачем нужно было изобретать что-то новое, если уже был log4j мне лично не понятно, однако в рамках формирования JSR 47 взяли не log4j, так появился JUL.

Логер включен в стандарт(в рамках JSR 47) и поставляется вместе с JDK. JUL имеет следующие уровни логгирования по возрастанию: FINEST, FINER, FINE, CONFIG, INFO, WARNING, SEVERE, а так же ALL и OFF, включающий и отключающий все уровни соответственно.

Вот эти все FINEST, FINER, FINE — это три уровня логирования для отладочных сообщений, три, Карл!

jul is fine

Чувствуете насколько все переосложнено?

JUL очень похож на log4j, но предоставляет гораздо меньше возможностей.

Так как стандартных средств форматирования логов недостаточно, то все сводилось к тому, что писались свои. Это при том, что log4j предоставлял больший функционал, работал как минимум не медленнее и в целом себя чувствовал хорошо.

Настраивается только с помощью properties.

И вот мы уже имеем два логгера, одни библиотеки использовали log4j, другие jul и это было начало хаоса.

Именно это послужило толчком к созданию Apache Commons Logging или JCL.

JCL

Как уже было сказано, JCL — это обертка над log4j и JUL.

Уровни логгирования у JCL совпадают с log4j, а в случае взаимодействия с JUL происходит следующее сопоставление:

JCL JUL
ALL Все сообщения
TRACE Level.FINEST
DEBUG Level.FINE
INFO Level.INFO
WARN Level.WARNING
ERROR Level.SEVERE
FATAL Level.SEVERE
OFF Без сообщения

Конфигурация JCL содержит отдельные блоки для log4j, JUL и собственной реализации.

Внутри себя она часто использует reflection, поэтому проседает по производительности и утяжеляет приложение.

JCL на данный момент почти никогда не встречается в новых проектах, это довольно старая библиотека, которая встречается разве что в старых legacy-проектах.

С уверенностю можно сказать сейчас, что в эту сторону даже смотреть не стоит. Пациент мертв.

Разработчик JCL даже как-то высказался в духе: Commons Logging was my fault.

Лицензия Apache License Version 2.0
Последняя версия 1.2
Дата выпуска последней версии июль 2014

Apache log4j2

Какое-то время все так и существовало, но по мере попытки усидеть на двух стульях происходит раскол и создание log4j2 — на новых идеях, использующая все модные фишки.

При этом она оказывается полностью несовместима с log4j.

Но добавили много нового, парочка из них:

  • Система плагинов, которая позволяет добавить новые appender-ы, layout-ы и т.д
  • Улучшения производительности.
  • Появилась поддержка конфигруаций через json и yaml.
  • Поддержка jmx.

Полный список тут.

Правда перестали поддерживать properties конфигурации и конфигурации от log4j на xml надо было переписывать заново.

На данный момент рекомендуется использовать именно log4j2. Однако надо помнить, что Log4j 2 работает только с Java 6+.

Лицензия Apache License Version 2.0
Последняя версия 2.11.2
Дата выпуска последней версии февраль 2019

SLF4J

Появление нового логера и проблемы JCL послужили появлению slf4j — еще одной обертке.

Помимо того, что она не имеет проблем с производительностью, как у JCL, является оберткой над всеми известными логерами типа logback, log4j, jul и т.д, она предоставляет еще параметризованные сообщения:

log.debug("User {} connected from {}", user, request.getRemoteAddr());

При этом преобразование параметров в строку и окончательное форматирование лог-записи происходит только при установленном уровне DEBUG.

Лицензия MIT License
Последняя версия 2.0.0-alpha0
Дата выпуска последней версии июнь 2019

Logback

logback был сделан разработчиком log4j, поэтому многие фишки перекочевали сразу, а учитывая, что разрабатывался он позже, то старческие болячки log4j обошли.

При этом, logback не является частью Apache или еще какой-то компании и независим.

Может быть сконфигурирован через xml и groovy.

Для logback:

Лицензия EPL/LGPL 2.1
Последняя версия 1.3.0-alpha4
Дата выпуска последней версии февраль 2018

На данный момент это самый мощный логер по количеству предоставляемых возможностей ‘из коробки’. Однако, из-за некоторой его обособленности и сложной лицензии многие опасаются его использования и предпочитают log4j или log4j2.

В данный момент все чаще встречаются проекты, которые выбрали связку SLF4J + Logback.

При этом в версии 1.2.3 стоит явное ограничение на количество файлов, с которым умеет работать логер при ротировании — это трехзначное число, в поздней версии это ограничение убрали.

Что выбрать

Сейчас я бы выбрал связку SLF4J и logback, так как при необходимости вы сможете переключиться с logback на другую реализацию логера. При этом logback обладает всеми преимуществами slf4j, но без старых болячек и с дополнительными плюшками.

Из минусов logback — это лицензия(LGPL/EPL) и то, что он независимый. Т.е он не принадлежит ни Apache, ни каким-то еще компаниям, а это для некоторых может стать серьезным минусом.

Если минусы для вас существенны, то я бы выбирал log4j2 или log4j.

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

Разбираем SLF4J

Так как из адаптеров это по сути единственный выбор, да и встречается slf4j все чаще, то стоит рассмотреть его устройство.

Вся обертка делится на две части — API, который используется приложениями, и реализация логера, которая представлена отдельными jar-файлами для каждого вида логирования. Такие реализиации для slf4j называются binding. Например, slf4j-log4j12 или logback-classic.

Достаточно только положить в CLASSPATH нужный binding, после чего — опа! весь код проекта и все используемые библиотеки (при условии, что они обращаются к SLF4J) будут логировать в нужном направлении.


Вопрос:

А что будет, если в CLASSPATH окажется несколько binding-ов?

Ответ:

SLF4J найдет все доступные binding-и и напишет об этом, после чего выберет какой-то и тоже об этом напишет.

Вот пример поведения slf4j, когда в CLASSPATH оказалось два binding: logback и slf4j-log4j12:

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.2.3/7c4f3c474fb2c041d8028740440937705ebb473a/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-log4j12/1.7.26/12f5c685b71c3027fd28bcf90528ec4ec74bf818/slf4j-log4j12-1.7.26.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.

SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]

Что делать при такой ситуации. Если коротко, то надо просто вычистить из CLASSPATH-а все ненужные binding-и и оставить только один.

Т.е вы строите дерево зависимостей проекта, после чего методично вырезаем все, что нам не нужно в CLASSPATH-е.

Вроде все проблемы решены, пусть и такими радикальными способоами.

Но это еще не все.

log hell

Проблема

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

Например, Spring использует адаптер jcl.

И вот в такой ситуации, чтобы все работало с slf4j используют так называемые bridge-ы.

Что такое bridge? Это jar-ник, который кладется в CLASSPATH вместо настоящей библиотеки логирования, в этом jar-нике все классы, которые существуют в настойщей библиотеке логирования, но они просто делегируют все события логирования в slf4j.

Например, jcl-over-slf4j.jar, log4j-over-slf4j.jar или jul-to-slf4j.jar, которые переопределяют поведение соответствующих логгеров и перенаправляют сообщения в slf4j.

Таким образом, чтобы работать со Spring получается надо сделать CLASSPATH подобным образом:

    compile "org.slf4j:jcl-over-slf4j:$slf4_version"
    exclude group: "commons-logging", module: "commons-logging"

Т.е исключить из CLASSPATH уже не нужный нам jcl, после чего добавить bridge, чтобы он перенаправлял все события логирования Spring в slf4j.

Подробнее об этом.

Проблемы bridge-а:

  • Если конфигурация сделана программно, то bridge не будет работать.

  • В CLASSPATH может оказаться и binding, и bridge на один и тот же логгер. В этом случае они начнут бесконечно перекидывать друг другу сообщения логирования, пока не свалятся на StackOverflowError.

    Например, log4j-over-slf4j.jar и slf4j-log4j12 в одном CLASSPATH приведут к StackOverflowError.

  • Проблема с JUL.

    Если вы внимательно читали то, что мы говорили про bridge, то уже поняли в чем дело: bridge по сути подменяет классы, а подменить классы jdk — нельзя.

    Поэтому bridge для jdk логера работает иначе — устанавливается специальный обработчик на root логер, который заворачивает события в slf4j.

    И все бы ничего, но такой обработчик заворачивает все события от jul, даже те, для которых не указаны appender-ы. Отсюда мы получаем большой overhead и проседает производительность.

log final feel

Заключение

Проблема логирования в Java стоит остро до сих пор. В том, что появилась такая проблема как многообразие логеров и отсутствие какого-то внятного стандарта виноваты все, в том числе и, на мой взгляд, какие-то политические моменты, как например с JSR 47.

Бесконтрольно подтягиваемые транзитивные зависимости библиотек, которые вы используете в своем проекте, рано или поздно принесут какие-то свои библиотеки логирования, отчего могут открыться врата прямиком в ад.

Поэтому следите за CLASSPATH, смотрите что вы используете и не разводите log-зоопарк.

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

При этом, если вы разрабатываете библиотеку, то:

  • Не используейте jul.
  • Задумайтесь о том, что нужна ли библиотека логирования, которую вы используете, другим людям как транзитивная зависимость вашей библиотеки?

Очень важно также не забывать о том, что такое логирование и для чего оно нужно.

Поэтому нельзя скатываться в бесмысленные записи в лог, вывод личных данных и так далее.

Думайте о том что вы пишите в лог!

Полезные ссылки

  1. Java Logging: история кошмара
  2. Владимир Красильщик — Что надо знать о логировании прагматичному Java-программисту
  3. Ведение лога приложения
  4. Java logging. Hello World

При создании приложений мы часто сталкиваемся с ошибками, которые необходимо отлаживать. Итак, с помощью логов мы можем легко получить информацию о том, что происходит в приложении, с записью ошибок и необычных обстоятельств. Теперь вам может показаться, что почему бы не использовать оператор System.out.print() в Java.

Проблема с этими утверждениями состоит в том, что сообщения журнала будут печататься только на консоли. Поэтому, как только вы закроете консоль автоматически, все журналы будут потеряны. Они не хранятся постоянно и будут отображаться один за другим, так как это однопоточная среда.

Чтобы избежать таких проблем, логирование в Java упрощается с помощью API, предоставляемого через пакет java.util.logging пакет org.apache.log4j.* .

Компоненты

Компоненты ведения журнала помогают разработчику создавать их, передавать в соответствующее место назначения и поддерживать надлежащий формат. Ниже приведены три компонента:

  • Loggers – отвечает за сбор записей журнала и передачу их соответствующему заявителю.
  • Appenders или Handlers – они отвечают за запись событий журнала в пункт назначения. Аппендеры форматируют события с помощью макетов перед отправкой результатов.
  • Layouts или Formatters – отвечает за определение того, как данные выглядят, когда они появляются в записи журнала.

Вы можете обратиться к изображению ниже для работы всех трех компонентов:

Компоненты Logger в Java

Когда приложение выполняет вызов регистрации, компонент Logger записывает событие в LogRecord и перенаправляет его соответствующему Appender. Затем он форматировал запись, используя формат в соответствии с требуемым форматом. Помимо этого, вы также можете использовать более одного фильтра, чтобы указать, какие Appenders следует использовать для событий.

Логгеры (Logger) в Java – это объекты, которые запускают события журнала. Они создаются и вызываются в коде приложения, где генерируют события журнала перед передачей их следующему компоненту, который является Appender.

Вы можете использовать несколько логгеров в одном классе для ответа на различные события или использовать в иерархии. Они обычно называются с использованием иерархического пространства имен, разделенных точками. Кроме того, все имена Logger должны основываться на классе или имени пакета зарегистрированного компонента.

Кроме того, каждый логгер отслеживает ближайшего существующего предка в пространстве имен, а также имеет связанный с ним «уровень».

Как создать?

Вы должны использовать Logger.getLogger() . Метод getLogger() идентифицирует имя Logger и принимает строку в качестве параметра. Таким образом, если Logger уже существует, он возвращается, в противном случае создается новый.

Синтаксис

static Logger logger = Logger.getLogger(SampleClass.class.getName());

Здесь SampleClass – это имя класса, для которого мы получаем объект Logger.

Пример:

public class Customer{
    private static final Logger LOGGER = Logger.getLogger(Customer.class);
    public void getCustomerDetails() {
    }
}

Уровни

Уровни журналов используются для классификации их по степени серьезности или влиянию на стабильность приложения. Пакет org.apache.log4j.* и java.util.logging предоставляют разные уровни ведения журнала.

Пакет org.apache.log4j.* предоставляет следующие уровни в порядке убывания:

  • FATAL;
  • ERROR;
  • WARN;
  • INFO;
  • DEBUG.

Пакет java.util.logging предоставляет следующие уровни в порядке убывания:

  • SEVERE(HIGHEST LEVEL);
  • WARNING;
  • INFO;
  • CONFIG;
  • FINE;
  • FINER;
  • FINEST(LOWEST LEVEL).

Помимо этого, вышеприведенный пакет также предоставляет два дополнительных уровня ALL и OFF используются для регистрации всех сообщений и отключения регистрации соответственно.

Пример с использованием пакета org.apache.log4j.*

import org.apache.log4j.Logger;
public class Customer {
    static Logger logger = Logger.getLogger(Customer.class);
    public static void main(String[] args) { 
	logger.error("ERROR");
        logger.warn("WARNING");	
	logger.fatal("FATAL");
        logger.debug("DEBUG");
        logger.info("INFO");
        System.out.println("Final Output");
    }
}

Таким образом, если в нашем файле log4j.properties ваш вывод является корневым логгером уровня WARN, то все сообщения об ошибках с более высоким приоритетом, чем WARN, будут напечатаны, как показано ниже:

Вы также можете установить уровень с помощью метода setLevel() из пакета java.util.logging , как java.util.logging ниже:

logger.setLevel(Level.WARNING);

Пример с использованием пакета java.util.logging

package edureka;
import java.io.IOException; 
import java.util.logging.Level; 
import java.util.logging.Logger; 
import java.util.logging.*; 
  
class EdurekaLogger { 
    private final static Logger LOGGER =  Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);   
    public void sampleLog() 
    { 
        LOGGER.log(Level.WARNING, "Welcome to Edureka!"); 
    } 
}   
public class Customer { 
    public static void main(String[] args) 
    { 
        EdurekaLogger obj = new EdurekaLogger(); 
        obj.sampleLog(); 
        LogManager slg = LogManager.getLogManager();        
        Logger log = slg.getLogger(Logger.GLOBAL_LOGGER_NAME);   
        log.log(Level.WARNING, "Hi! Welcome from Edureka"); 
    } 
} 

Чтобы включить вход в приложение с помощью пакета org.apache.log4j.* Или пакета java.util.logging , необходимо настроить файл свойств. Далее в этой статье о Logger в Java давайте обсудим файл свойств обоих из них.

Файл свойств пакета Log4j и Java Util

Пример файла свойств Log4j

# Enable Root logger option
log4j.rootLogger=INFO, file, stdout
# Attach appenders to print file
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=E:loglogging.log
log4j.appender.file.MaxFileSize=10MB
log4j.appender.file.MaxBackupIndex=5
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
# Attach appenders to print on console
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
  • Файл свойств Log4j создается внутри папки src проекта.
  • log4j.appender.file = org.apache.log4j.RollingFileAppender -> Печатает все журналы в файле
  • log4j.appender.stdout = org.apache.log4j.ConsoleAppender -> Печатает все журналы в консоли
  • log4j.appender.file.File = D: loglogging.log -> Указывает расположение файла журнала
  • log4j.appender.file.MaxFileSize = 10 МБ -> Максимальный размер файла журнала до 10 МБ
  • log4j.appender.file.MaxBackupIndex = 5 -> Ограничивает количество файлов резервных копий до 5
  • log4j.appender.file.layout = org.apache.log4j.PatternLayout -> Указывает шаблон, в котором журналы будут печататься в файл журнала.
  • log4j.appender.file.layout.ConversionPattern =% d {гггг-ММ-дд ЧЧ: мм: сс}% -5p% c {1}:% L -% m% n -> Устанавливает шаблон преобразования по умолчанию.

Пример файла свойств пакета Java Util

handlers= java.util.logging.ConsoleHandler

.level= WARNING

# Output will be stored in the default directory
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 60000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# Level of logs will be limited to WARNING and above.
java.util.logging.ConsoleHandler.level = WARNING
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

Здесь

  • java.util.logging.FileHandler.pattern =% h / java% u.log -> Файлы журнала будут записаны в C: TEMPjava1.log
  • java.util.logging.FileHandler.limit = 50000 -> Максимальная сумма, которую регистратор записывает в один файл в байтах.
  • java.util.logging.FileHandler.count = 1 -> Указывает количество выходных файлов
  • java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter -> Упоминает форматер, используемый для форматирования. Здесь используется XML Formatter.
  • java.util.logging.ConsoleHandler.level = WARNING -> Устанавливает уровень журнала по умолчанию для WARNING
  • java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter -> Указывает форматер, используемый всеми ConsoleHandler. Здесь используется SimpleFormatter.

Регистрация событий

Чтобы регистрировать события в Java, вы должны убедиться, что вы назначаете уровень, чтобы легко отфильтровать события. Чтобы назначить уровень и упомянуть сообщение, вы можете использовать следующие методы:

Способ 1

logger.log(Level.INFO, “Display message”);
Here, level is INFO and the message to be printed is "Display Message".

Способ 2

logger.info(“Display message”);

Чтобы убедиться, что Logger регистрирует только события, которые находятся на уровне или выше уровня INFO, вы можете использовать метод setLevel(), описанный выше.

Appender или Handlers

Appender или Handlers отвечают за запись событий журнала в пункт назначения. Каждый регистратор имеет доступ к нескольким обработчикам и получает сообщение журнала от регистратора. Затем Appenders используют средства форматирования или макеты для форматирования событий и отправки их в соответствующее место назначения.

Appender можно отключить с помощью метода setLevel (Level.OFF). Два наиболее стандартных обработчика в пакете java.util.logging :

  • FileHandler: записать сообщение журнала в файл.
  • ConsoleHandler: записывает сообщение журнала в консоль.

Layout или Formatters

Используются для форматирования и преобразования данных в журнале событий. Каркасы журналов предоставляют макеты для HTML, XML, Syslog, JSON, простого текста и других журналов.

Where are Java Errors Logged?

Logging Java errors is an essential component in any application as it allows developers to track the root cause of errors and fix them easily. By default, Java log messages are stored only on the console, but they can be transferred to a longer term location as well. These messages let you see what’s happening in your application and troubleshoot problems.

Console logs in Java

At the most basic level, Java errors are logged on the console. Developers typically call System.out.println() to print log messages on the console. Whenever the console is closed, these messages are lost as they cannot be stored in any permanent location. To overcome this problem, developers use Java logging frameworks which are responsible for storing data to any other location such as a file or a database.

Java Logging frameworks

There are several common frameworks for logging in Java and they let you customize where your logs are sent. Logback and Log4j are two popular Java logging frameworks that are simple to use. Logback was built as a replacement for its predecessor, Log4j. Logback offers a faster implementation than Log4j, provides more options for configuration, and gives more flexibility in archiving old log files. The newest is log4j 2 but it’s still catching up in adoption.

Let’s focus on Logback since it offers many improvements and is very common. The Logback architecture consists of three main classes: logger, appender, and layout. The appender determines where the logs are sent.

A logger is a class that applications interact with to create log messages. Layouts prepare messages for outputting. Logback supports the creation of custom classes for formatting messages, as well as robust configuration options for the existing ones. Appenders transfer log messages to their final destinations. A logger can have more than one appender such as ConsoleAppender, FileAppender, RollingFileAppender, etc. We generally think of appenders as being attached to text files, but Logback also offers network outputs like syslog.

A Java logging framework like Logback is declared as a dependency. When using Maven, it’ll be added in the pom.xml file.

<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.2.3</version>
</dependency>

The configuration file is where the layouts and appenders are declared. Look for a text file named logback.xml. It should be located somewhere in the project classpath. In this example, you can see it’s set to log to standard output on the console:

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
  <root level="debug">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Log files

With Logback, log files can be created by adding a file appender in the configuration file.

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
  <file>tests.log</file>
  <append>true</append>
  <encoder>
    <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
  </encoder>
</appender>

In the example below, we’ve created a log file named test.log that generates an example message stored in the test.log file.

public class Example {
  private static final Logger logger = LoggerFactory.getLogger(Example.class);

  public static void main(String[] args) {
    logger.info("Example log from {}", Example.class.getSimpleName());
  }
}

Here is the log message of test.log file which contains the date and time of the message and a description of the generated message:

20:34:22.136 [main] INFO Example - Example log from Example

Log levels in Java

There are five different levels in the logger class. Each level has a different priority; from lowest to highest they are debug, info, warn, error, and fatal. This is useful to tune what level of detail you wish to view or record in the logs, and can be helpful to filter on the highest impact problems. If you are debugging a Java error you’ll want debug logs, but production applications may run more efficiently if you record only info and above.

Fatal:

This is the highest priority log message, a severe issue that causes premature termination. These issues are expected to be immediately visible on the Java console. logger.fatal("Fatal Message") method is used to display this type of message.

Error:

These are runtime Java errors or unexpected conditions that need to be caught or the application will exit. They often indicate bugs or failures in dependent systems. We can display the Java error messages using the logger.error("Error Message") method.

Warning:

This type of log is at a level lower than the error level in Java. It often arises due to the use of deprecated APIs and other runtime situations that are undesirable but the program is able to continue processing. The logger.warn("Warning Message") method displays this type of message.

Info:

This displays the informational messages to indicate the progress of an application using the logger.info("Information Message") method. It shows the Java runtime events such as startup or shutdown. Informational events often give clues to the cause of following errors.

Debug:

logger.debug("Debug Message") shows the most verbose information, which is useful to debug the Java application. Leaving this level on all the time can produce a lot of noisy data.

Java Log formats

The major problem with log files is that by default, they are typically unstructured text data. This makes it hard to query them for any sort of useful information. As a developer, it would be nice to be able to filter all logs by the application name or the severity of the event. The goal of log formats is to solve these sorts of problems and allow additional analytics.

The format of Java logs is determined by the logging framework you use. Let’s look at the format of log4j logs. A simple example will help make it clear what log formatting really is. Generally you write to a log file as shown below:

public class TestClass {
  private static final Logger logger = LoggerFactory.getLogger(TestClass.class);
  public static void main(String[] args) {
    logger.debug("This is debug");
  }
}

This example would simply produce a message in your Java logs, which isn’t very useful for troubleshooting or analysis.

This is debug

You can format the logs using the log4j pattern layout to add helpful metadata. To do this you need to add a ConversionPattern in the log4j.properties file.

log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d %p %t %c %m

In the example above, the ConversionPattern contains a combination of conversion characters: %d denotes date and time, %p is for log level, %t is used to print the name of the thread, %c is used to output the name of class, and %m is used to print the application-supplied message associated with the logging event.

2018-12-15 01:35:32,279 DEBUG main com.example.demo.TestClass This is debug

The Java log starts with the date and time of log. This is followed by the log level, whether it is INFO, ERROR, WARNING, FATAL, or DEBUG. It contains the method name and source class. It also contains the log message giving a brief description about the log. This additional information tells us when and where the message was generated.

To make log files machine readable and to add more advanced functionalities, we recommend using a structured format that can be easily parsed. This could be XML, JSON, or other formats. JSON is the most popular, so you are most likely to see JSON for structured logging. Most log management solutions like Loggly and Splunk can ingest and index each of the fields for fast searching.

With logback, you can specify a JSON layout along with the required logging fields. It will produce a JSON log as shown below:

{ "time": "2018-12-04 21:42:20", "level": "DEBUG ", "message": "This is debug" }

Track, Analyze and Manage Java Errors With Rollbar

Logging Java errors and exceptions in your code is challenging. It can make deploying production code an unnerving experience. Being able to track, analyze, and manage Java errors in real-time can help you to proceed with more confidence. Rollbar automates error monitoring and triaging, making fixing Java errors easier than ever. Try it today.

Понравилась статья? Поделить с друзьями:

Не пропустите эти материалы по теме:

  • Яндекс еда ошибка привязки карты
  • Логирование ошибки это
  • Логико стилистические ошибки примеры
  • Логико семантические ошибки
  • Логика ошибки утверждений

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии