Hi! When writing the lessons, I particularly emphasize if there is a specific topic that will be absolutely essential in real work. Why we need logging - 1 So, LISTEN UP! The topic we'll cover today will definitely come in handy in all your projects from day one of employment. We're going to talk about logging. This topic isn't at all complicated (I would even say easy). But you'll have enough obvious things to stress about at your first job, so it's better to thoroughly understand it right now :) Well, let's begin. What is logging? Logging is the act of recording data about the operation of a program. The place where we record this data is called a "log". Two questions immediately arise: what data is written and where? Let's start with the "where". You can write data about a program's work in many different places. For example, during your studies, you often System.out.println() to output data to the console. This is indeed logging, albeit in its simplest form. Of course, this isn't very convenient for users or a product support team: obviously, they won't want to install an IDE and monitor the console :) There is a more customary format for recording information: text files. Humans are much more comfortable reading data in this format, and certainly it is much more convenient for storing data! Now the second question: what program data should be logged? That's entirely up to you! Java's logging system is very flexible. You can configure it to log everything that your program does. On the one hand, this is good. But on the other hand, imagine how large Facebook's or Twitter's logs would be if they wrote everything in them. These large companies probably do have the ability to store that much data. But imagine how difficult it would be to find information about one critical error in 500 gigabytes of text logs? That would be worse than looking for a needle in a haystack. Accordingly, Java can be configured to log only error data. Or even just critical errors! That said, it is not entirely accurate to speak of Java's native logging system. The fact is that programmers needed logging before this functionality was added to the language. By the time Java introduced its own logging library, everyone was already using the log4j library. The history of logging in Java is actually very long and informative. In short, Java has its own logging library, but almost no one uses it :) Later, when several different logging libraries appeared and began to be used by programmers, compatibility problems arose. To stop people from reinventing the wheel in a dozen different libraries with different interfaces, the abstract SLF4J framework ("Service Logging Facade For Java") was created. It's called abstract, because even if you use and call the methods of SLF4J classes, under the hood they actually use all the logging frameworks that came before: log4j, the standard java.util.logging, and others. If at some point you need some specific feature of Log4j lacking in other libraries, but you don't want to directly link your project to this library, just use SLF4J. And then let it call the Log4j methods. If you change your mind and decide that you no longer need Log4j features, you only need to reconfigure the "wrapper" (i.e. SLF4J) to use another library. Your code won't stop working, because you're calling the SLF4J methods, not a specific library. A small digression. For the following examples to work, you need to download the SLF4J library here, and the Log4j library here. Next, unpack the archive and use IntelliJ IDEA to add the JAR files to the classpath. Menu items: File -> Project Structure -> Libraries Select the necessary JAR files and add them to the project (the archives we downloaded contain many JAR files — look at the pictures to see the ones you need) Why we need logging - 2Why we need logging - 3Note that this instruction for those students who don't know how to use Maven. If you know how to use Maven, it's usually better (much easier) to try to start there. Great! We figured out the settings :) Let's see how SLF4J works. How do we make sure that the program's work is recorded somewhere? To do this, we need two things: logger and appender. Let's start with the first. A logger is an object that provides full control of logging. Creating a logger is very easy: we do this using the static LoggerFactory.getLogger() methods. The method parameter is the class whose operation will be logged. Let's run our code:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyTestClass {

   public static final Logger LOGGER = LoggerFactory.getLogger(MyTestClass.class);

   public static void main(String[] args) {

       LOGGER.info("Test log entry!!!");
       LOGGER.error("An error occurred!");
   }
}
Console output:
ERROR StatusLogger No Log4j 2 configuration file found. Using default configuration (logging only errors to the console), or user programmatically provided configurations. Set system property 'log4j2.debug' to show Log4j 2 internal initialization logging. See https://logging.apache.org/log4j/2.x/manual/configuration.html for instructions on how to configure Log4j 2 15:49:08.907 [main] ERROR MyTestClass - An error occurred!
What do we see here? First, we see an error message. This is a result of the fact that now we're lacking the necessary settings. Accordingly, our logger is currently only able to output error messages (ERROR) and only to the console. The logger.info() method didn't work. But logger.error() did! On the console, we see the current date, the method where the error occurred (main), the word "ERROR", and our message! ERROR is the logging level. In general, if a log entry is marked with the word "ERROR", then an error has occurred at this point in the program. If the entry is marked with the word "INFO", then the message simply represents current information about the normal operation of the program. The SLF4J library has a lot of different logging levels that let you configure logging flexibly. It's all very easy to manage: all the necessary logic is already in the Logger class. You just need to call the relevant methods. If you want to log a routine message, call the logger.info() method. For an error message, use logger.error(). For a warning, use logger.warn() Now let's talk about appender. An appender is the place where your data goes. In a way, the opposite of a data source, i.e. "point B". By default, data is output to the console. Note that in the previous example we didn't have to configure anything: the text appeared in the console, and the Log4j library's logger can only output ERROR-level messages to the console. Obviously, it's more convenient for people to read and write logs in a text file. To change the logger's default behavior, we need to configure our file appender. To get started, you need to create a log4j.xml file directly in the src folder. You're already familiar with the XML format: we recently had a lesson about it :) Here are the contents of the file:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
   <Appenders>
       <File name="MyFileAppender" fileName="C:\Users\Username\Desktop\testlog.txt" immediateFlush="false" append="false">
           <PatternLayout pattern="%d{yyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
       </File>
   </Appenders>
   <Loggers>
       <Root level="INFO">
           <AppenderRef ref="MyFileAppender"/>
       </Root>
   </Loggers>
</Configuration>
Nothing particularly special or difficult here :) But still, let's go through the content.
<Configuration status="INFO">
This is the so-called StatusLogger. It is unrelated to our logger and is used in Log4j's internal processes. If you set status="TRACE" instead of status="INFO", and all information about Log4j's internal work will be displayed on the console (StatusLogger displays data on the console, even if our appender is a file). We don't need it now, so let's leave it as it is.
<Appenders>
   <File name="MyFileAppender" fileName="C:\Users\Evgeny\Desktop\testlog.txt" append="true">
       <PatternLayout pattern="%d{yyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
   </File>
</Appenders>
Here we create our appender. The <File> tag indicates that it will be a file appender. name="MyFileAppender" sets the name of the appender. fileName="C:\Users\Username\Desktop\testlog.txt" indicates the path to the log file where all the data will be written. append="true" indicates whether the data should be written at the end of the file. In our case, this is precisely what we'll do. If you set the value to false, then the old contents of the log file will with deleted each time the program is started. <PatternLayout pattern="%d{yyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> indicates the formatting settings. Here we can use regular expressions to customize how text is formatted in our log.
<Loggers>
       <Root level="INFO">
           <AppenderRef ref="MyFileAppender"/>
       </Root>
</Loggers>
Here we indicate the root level. We have set the "INFO" level, which means that all messages whose levels are higher than INFO (according to the table that we looked at above) will not be logged. Our program will have 3 messages: one INFO, one WARN and one ERROR. With the current configuration, all 3 messages will be logged. If you change the root level to ERROR, only the last message from the LOGGER.error() method call will end up in the log. Additionally, a reference to the appender also goes here. To create such a reference, you need to create a <ApprenderRef> tag inside the <Root> tag and add the ref='your appender's name' attribute to it. In case you forgot, this is where we set the appender's name: <File name="MyFileAppender". And here's our code!
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyTestClass {

   public static final Logger LOGGER = LoggerFactory.getLogger(MyTestClass.class);

   public static void main(String[] args) {

       LOGGER.info("The program is starting!!!");

       try {
           LOGGER.warn("Attention! The program is trying to divide a number by another.
           System.out.println(12/0);
       } catch (ArithmeticException x) {

           LOGGER.error("Error! Division by zero!");
       }
   }
}
Of course, it's a little whacky (catching a RuntimeException is a questionable idea), but it's perfect for our purposes :) Let's run our main() method 4 times in a row and look at our testlog.txt file. You don't need to create it in advance: the library will do this automatically. Everything worked! :) Now you have a configured logger. You can play around with some of your old programs, adding logger calls to each method. Then look at the resulting log :) It considers the topic of logging in depth. It would be challenging to read it all in one sitting. That said, it does contain a lot of additional useful information. For example, you'll learn how to configure the logger so that it creates a new text file if our testlog.txt file reaches a certain size :) And that concludes our class! Today you became familiar with a very important topic, and this knowledge will definitely be helpful to you in your future work. Until next time! :)