Chapters
Overview
Logging is an essential part of developing an application. It lets us diagnose applications with ease. There are thid-party loggers that we can export to java such as
Log4j2. In this tutorial, we're gonna use
java.util.logging package to create loggers.
Creating a Logger
Creating a logger is easy. We just need to invoke
getLogger
method from
Logger class. Take a look at this example.
import java.util.logging.*;
public class SampleClass{
//a Logger instance is preferrable to be referenced
//to a global variable to prevent garbage collector
//from collecting the logger's instance
//in unexpected manner
static Logger logger =
Logger.getLogger(SampleClass.class.getName());
public static void main(String[] args){
try{
int num = 1/0;
}
catch(ArithmeticException e){
logger.log(Level.SEVERE, "Divisor is 0!", e);
}
}
}
Result
May 17, 2022 4:40:44 PM SampleClass main
SEVERE: Divisor is 0!
java.lang.ArithmeticException: / zero
at SampleClass.main(SampleClass.java:10)
getLogger(String name)
creates a logger with the specified name. If the logger with the specified name is already created, this method gets that logger instead of creating a new one.
Loggers are normally named, using a hierarchical dot-separated namespace. Logger names can be arbitrary strings, but they should normally be based on the package name or class name of the logged component, such as
java.net
or
javax.swing
.
log
method logs a message. When the message is logged, it will be passed to logger's handler and the handler. What is a handler? handlers process the logged messages and output the messages to a console, file, etc. We will learn handlers later. In the example above, we didn't specify any handler to our logger. Thus, our logger uses the default handler which outputs messages to a console.
In the example above,
log(Level level, String msg, Throwable thrown)
method logged the date and time where the logging happened, the source class or the class where the log happened, the method name where the log happened, level which we will learn later, the message and the exception that we put in the
log
method.
log
method has other variants:
logp
and
logrb
methods.
logp
or "log precise" method is just like
log
method but the source class and source method needs to be specified explicitly.
logrb
method is just like
logp
method but this method accepts a
ResourceBundle. Resource bundles contain locale-specific objects.
Some methods use
MessageFormat style to format parameters in the
params
or
param
argument. For example, let's invoke this method:
log(Level level, String msg, Object[] params)
logger.log(Level.SEVERE, "Causes: {0}, {1}, {2}", new String[]{"Divisor is 0!", "Illegal Division", "Undefined"});
If you put this code in the example above, the result would be:
May 17, 2022 4:40:44 PM SampleClass main
SEVERE: Causes: Divisor is 0!, Illegal Division, Undefined
If we just casually trying out some things and don't wanna create a logger, we can use the global logger. This logger is designed for testing purposes. To get the global logger, we can invoke
Logger.getGlobal()
or use
GLOBAL_LOGGER_NAME
field. For example:
//via method
Logger logger = Logger.getGlobal();
//via constant
Logger logger =
logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
//via String literal
Logger logger =
logger.getLogger("global");
"global" is the name the refers to the global logger. In production, it's recommended to create logger per top-level class and add the class names to the loggers' name.
Levels
The
Level class defines a set of standard logging levels that can be used to control logging output. These are the levels in descending order:
- SEVERE (highest value)
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST (lowest value)
To get logger's level, invoke
getLevel
method from Logger class. To set logger's level, invoke
setLevel(Level newLevel)
from Logger class. Additionally, two other constants, apart from the constants above, can be used in
setLevel
method. These are:
Level.OFF
means that all logging levels from FINEST to SEVERE are not loggable.
Level.ALL
is the opposite of
Level.OFF
. When we set a level to be loggable by invoking
setLevel
, all levels on top of the level that has been set will be loggable. For example, if I invoke
setLevel
like this:
setLevel(Level.INFO)
. Then WARNING and SEVERE are also going to be loggable.
Note that loggers and handlers have their own levels. The level of a logger determines if a log can be sent to the handlers. The level of a handler determines if the log that has been received will be exported to a console, file, etc.
If the return value of
setLevel
method is null, it means that the handler or the logger will use the level of its parent logger. If a level is loggable, the handler or log accepts logs with the specified level. Otherwise, the log will be ignored.
According to the documentation, FINEST, FINER and FINE are used to log messages regarding tracing messages such as debug point or some kind of marker in message form. These levels are pretty optional and can be hidden.
CONFIG is used to log messages regarding configurations, INFO is used to log informational messages, WARNING is used to log potential problem and SEVERE is used to log serious failure such as exceptions. These levels are pretty important and should be watched closely especially the SEVERE level.
Logger Hierarchy
When we create a logger, that logger wil be added to the LogManager namespace. Java registers all created loggers to this namespace. However, there's a logger that can't be registered in the namespace and that logger is an anonymous logger.
One of the disadvantages of a logger that is not registered in the namespace is that that logger won't receive some checks like security checks which are done in the namespace. If you intend to bypass some checks then you may need to use anonymous logger to achieve your specific goal.
To create an anonymous logger, invoke
getAnonymousLogger
method from Logger class.
Now, let's talk about logger hierarchy. When we create a logger, the logger will be automatically become a childen of root logger. Root logger is a logger that is closest to the LogManager. We will learn about LogManager later. The default level of root logger is INFO if the "Default global logging level" attribute in the configuration file of LogManager is not specified.
Root logger was the reason we could print logs on the console. By default, a newly created logger doesn't have a handler. If a logger doesn't have a specified handler, it will pass the log to the handler of its parent logger. Take note that if the logger and its parent have handlers, the log will be passed to the handlers of the logger and its parent.
If we want a log to only be passed to our logger, invoke
setUseParentHandlers
method and set its argument to false. In this way, the log won't be passed to the parent of our logger.
To gain access to the root logger, invoke
Logger.getLogger()
with "" argument. For example:
Logger logger = Logger.getLogger("");
Root logger doesn't have a display name. That's why when we invoke
getName
method in root logger the result is blank. If we want to get the parent logger of a logger, invoke
getParent
method for Logger class. If the return value of the method is null, the logger doesn't have a parent logger or the logger is the root logger because root logger doesn't have a parent logger.
Take note that even anonymous logger is not part of the logging namespace, its parent is still the root logger. If we want to set a parent logger to another logger, invoke
setParent
method from Logger class. This example demonstrates
getParent
and
setParent
methods.
import java.util.logging.*;
public class SampleClass{
static Logger logger =
Logger.getLogger(SampleClass.class.getName());
public static void main(String[] args){
System.out.println("Name: " + logger.getName());
System.out.println("Parent: " +
logger.getParent());
System.out.println();
new SampleClass();
}
SampleClass(){
new Inner();
}
class Inner{
Logger logger =
Logger.getLogger(Inner.class.getName());
Inner(){
System.out.println("Name: " + logger.getName());
System.out.println("Parent: " + logger.getParent());
System.out.println();
System.out.println("Change Parent...");
logger.setParent(SampleClass.logger);
System.out.println("Parent: " +
logger.getParent().getName());
}
}
}
Result(may vary)
Name: SampleClass
Parent: java.util.logging.LogManager$RootLogger@194...
Name: SampleClass$Inner
Parent: java.util.logging.LogManager$RootLogger@194...
Change Parent...
Parent: SampleClass
If our class is in a package, the return value of
getName
method would be:
package-name.class-name
. Take note that the LogManager namespace manages the hierarchy of registered loggers.
Thus, the namespace will be updated if we change the hierarchy of the registered loggers. In the namespace, Loggers are organized into a naming hierarchy based on their dot separated names. Thus "a.b.c" is a child of "a.b", but "a.b1" and a.b2" are peers.
Configuring LogManager
LogManager is used to maintain a set of shared state about Loggers and log services. In every JVM, there is a single global LogManager object that maintains every logger and log service in our application.
We can get that object by invoking
LogManager.getLogManager
method. The LogManager object is created during class initialization and cannot subsequently be changed. One of the important functionalities of
LogManager
is that it loads a configuration file. During LogManager initialization, a configuration file sets up our logger framework based on the content of the file.
A configuration file can be in a class file or properties file. To explicitly assign a configuration file to our logger framework, we can type these commands on our terminal/cmd:
load class file
java -Djava.util.logging.config.class="class path"
load properties file
java -Djava.util.logging.config.file="file path"
If a configuration file is not explicity set. Java will use the default
logging.properties
file. In java 8 and below, this file is located in
$JAVA_HOME/jre/lib/
path. In java 9 and above, this file is located in
$JAVA_HOME/conf/
.
$JAVA_HOME
or
%JAVA_HOME%
(Windows) is a system variable that refers to the path where our JDK directory is located.
Now, let's create our own configuration file. First off, let's start with file configuration. Put this text in your custom properties file:
handlers = java.util.logging.ConsoleHandler
#ConsoleHandler initial configuration
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
#initialize new logger
initLogger.level = CONFIG
I'll explain the content of the properties file later. For now, create a file and name it as "MyConfig.properties" and put the file in a directory that you like. Then compile this java code.
import java.util.logging.*;
public class LogTest{
static Logger rootLogger =
Logger.getLogger("");
static Logger initLogger =
Logger.getLogger("initLogger");
static Logger globalLogger =
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
public static void main(String[] args){
System.out.println("Root: " + rootLogger.getLevel());
System.out.println("User: " + initLogger.getLevel());
System.out.println("Global: " + globalLogger.getLevel());
}
}
After that, type this command:
java -Djava.util.logging.config.file=C:\test\logging-config-test\MyConfig.properties LogTest
The result would be:
Root: INFO
User: CONFIG
Global: null
Take note that this command is also valid:
java LogTest -Djava.util.logging.config.file=C:\test\logging-config-test\MyConfig.properties
However, our config file won't be loaded once our program runs. We need to load the config file first before running the program. Thus, order of commands matters.
Java will use the default
logging.properties
if our config file can't be found.
java.util.logging.config.file
system property doesn't look at classpath. That's why we use absolute path to refer to our custom config file.
Next, let's talk about the properties in a config file. First off, the "#" symbol means a comment. LogManager ignores characters that are after this symbol. "handlers" is a LogManager property that assigns a handler to the root logger. If you want to assign a handler to a specific logger apart from root logger, we use this property "<logger>.handlers".
For example,
initLogger.handlers = java.util.logging.ConsoleHandler
Additionally, we can add multiple handlers in a logger. We separate the handlers using comma(,). For example:
handlers = java.util.logging.ConsoleHandler, java.util.logging.FileHandler
If you have a custom handler
handlers = MyPackage.MyHandler, java.util.logging.FileHandler
To initialize a new logger at startup, we just need to set one of the attributes of the new logger and java will create that logger for us. In the example above, I just write "initLogger.level = CONFIG" property and java creates a logger named "initLogger" and sets its level to CONFIG.
We can also set some properties of root and global logger in the config file. For example, if we want to change the level of the root logger, write this property: ".level = FINE". For global logger write this: "global.level = FINEST". Root and global loggers are automatically created by the LogManager.
In the discussion above, we now know that we can use "handlers" and "level" attributes to modify the handlers and levels of loggers. More information about properties of loggers can be found in the
LogManager documentation.
"java.util.logging.ConsoleHandler" system property refers to the
ConsoleHandler
. We can initialize some attributes of
ConsoleHandler
in the config file such as level, filter, formatter and encoding. Take note that each instance of
ConsoleHandler
will copy the values of the properties assigned to it in the config file during their initialization.
If we have custom handler, we can initialize some of their attributes in the config file. For example:
//if your custom handler is in a package
MyPackage.MyHandler.level = FINE
//if your custom handler is not in a package
MyHandler.level = FINE
In this way, each instance of
MyHandler
will have a FINE level. If the specified attribute of your custom handler in the config file doesn't match any attribute in the implementation of your custom handler, the property that specified the attribute is invalid and will be ignored.
For example, your custom handler is a
ConsoleHandler
and you specify "MyHandler.maxLocks" property. In this case, "maxLocks" attribute is gonna be ignored. "maxLocks" is an attribute of
FileHandler
. For more information about properties of ConsoleHandler can be found in this
documentation.
There are different types of handlers which we will learn in the next topic. We will also discuss formatters in another topic. Next, let's create a config file using class file. Write and compile this code:
import java.util.logging.*;
public class LoggerProperties{
public LoggerProperties(){
try{
//use
//System.setProperty(String key, String value) method
//to configure system property programatically
//
System.setProperty
("java.util.logging.ConsoleHandler.level","INFO");
//initialize a logger
Logger.getLogger("initLogger").
setLevel(Level.CONFIG);
//set global logger level
Logger.getLogger(Logger.GLOBAL_LOGGER_NAME).
setLevel(Level.FINE);
}
catch(Exception e){
e.printStackTrace();
}
}
}
After that, compile LogTest in the previous example and then type this command:
java -Djava.util.logging.config.class=LoggerProperties LogTest
And the result would be:
Root: INFO
User: CONFIG
Global: FINE
Use
System.setProperty(String key, String value)
to configure system properties. System properties are properties that are internally used by java. Unlike "java.util.logging.config.file" property, "java.util.logging.config.class" looks at classpath. That's why we can use a relative path relative to our program. Also, it's not required to include the .class file extension in the command.
There's another way to configure LogManager. We can use
readConfiguration
method in conjunction with "java.util.logging.config.file" property to directly load
properties
file in the classpath at startup. First off, compile this code:
import java.util.logging.*;
import java.io.InputStream;
public class LoggerProperties{
public LoggerProperties(){
//make variables final so they
//can't be changed.
//Thus, more security
try{
final LogManager lm =
LogManager.getLogManager();
try(
final InputStream is =
this.getClass().getResourceAsStream
("MyConfig.properties")){
lm.readConfiguration(is);
}
}
catch(Exception e){
e.printStackTrace();
}
}
}
After that, compile LogTest in the previous example and then type this command:
java -Djava.util.logging.config.class=LoggerProperties LogTest
Assuming
MyConfig.properties
has this content:
global.level = FINEST
#initialize new logger
initLogger.level = CONFIG
The result would be:
Root: INFO
User: CONFIG
Global: FINEST
Custom Handler
There are different types of handlers. All handlers implement
Handler class.
Handler
class has two direct subclasses:
MemoryHandler and
StreamHandler.
In this tutorial, we're gonna focus on
StreamHandler
. StreamHandler has three direct subclasses:
ConsoleHandler,
FileHandler and
SocketHandler. In this tutorial, we're going to focus on ConsoleHandler and FileHandler
First off, let's create a custom ConsoleHandler. Write and compile this code:
import java.util.logging.*;
import java.io.IOException;
public class SampleClass{
static Logger logger =
Logger.getLogger("MyLogger");
public static void main(String[] args){
ConsoleHandler ch =
new CustomConsoleHandler();
//add handler
logger.addHandler(ch);
//setLevel
logger.setLevel(Level.FINE);
ch.setLevel(Level.INFO);
//this wil ensure that the log sent
//to our logger won't be sent to
//its parent which is the root logger.
//
//If we don't do this, our console
//will receive two identical messages.
if(logger.getUseParentHandlers())
logger.setUseParentHandlers(false);
System.out.println("Logger Level: " +
logger.getLevel());
System.out.println("Handler Level: " +
ch.getLevel());
System.out.println();
logger.log(Level.FINE, "This is fine!");
}
}
class CustomConsoleHandler extends ConsoleHandler{
@Override
public void publish(LogRecord record){
super.publish(logRecord(record));
}
private LogRecord logRecord(LogRecord log){
if(log.getLevel() == Level.FINE){
Level newLvl = Level.INFO;
String message =
"\nOriginal Level: " +
log.getLevel() + "\n" +
"New level: " +
newLvl + "\n" +
"Message: " + "\n" +
log.getMessage();
LogRecord newLog =
new LogRecord(newLvl, message);
return newLog;
}
else return log;
}
}
Result
Logger Level: FINE
Handler Level: INFO
May 19, 2022 5:20:41 PM SampleClass main
INFO:
Original Level: FINE
New Level: INFO
Message:
This is fine!
We use
addHandler
method add handlers to our logger. Once our loggers creates a log, each handler in our logger will output the log.
LogRecord contains information about a log that is logged by a logger. Once all information about the log is wrapped to a LogRecord, the LogRecord will be sent to the handlers.
Now, let's try a custom FileHandler. Write and compile this code:
import java.util.logging.*;
import java.io.IOException;
public class SampleClass{
static Logger logger =
Logger.getLogger("MyLogger");
public static void main(String[] args)
throws IOException{
FileHandler ch =
new CustomFileHandler();
//add handler
logger.addHandler(ch);
//setLevel
logger.setLevel(Level.FINE);
ch.setLevel(Level.INFO);
logger.log(Level.INFO, "Seems like trouble!");
}
}
class CustomFileHandler extends FileHandler{
CustomFileHandler() throws IOException{}
@Override
public void publish(LogRecord record){
super.publish(logRecord(record));
}
private LogRecord logRecord(LogRecord log){
if(log.getLevel() == Level.INFO){
Level newLvl = Level.WARNING;
String message =
"\nOriginal Level: " +
log.getLevel() + "\n" +
"New level: " +
newLvl + "\n" +
"Message: " + "\n" +
log.getMessage();
LogRecord newLog =
new LogRecord(newLvl, message);
return newLog;
}
else return log;
}
}
Result(On console)
May 19, 2022 7:37:57 PM SampleClass main
INFO: Seems like trouble!
Result(in file)
<?xml version="1.0" encoding="windows-1252" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
<date>2022-05-19T11:37:57.256883500Z</date>
<millis>1652960277256</millis>
<nanos>883500</nanos>
<sequence>1</sequence>
<level>WARNING</level>
<class>SampleClass</class>
<method>main</method>
<thread>1</thread>
<message>
Original Level: INFO
New level: WARNING
Message:
Seems like trouble!</message>
</record>
</log>
If you have been noticed, the date&time in the file and console are different. In my opinion, java.util.logging.SimpleTimeFormatter and java.util.logging.XMLFormatter use different timezones.
In the example above, I use the default
logging.properties
configuration file. Let's examine some properties of FileHandler used in the configuration file.
<handler-name>.pattern specifies a pattern for generating the output file name. The default value of this property is "%h/java%u.log". "%h" denotes "user.home" system property in linux. In windows, it denotes "C:\Users\%USERNAME%" system property.
"%u" denotes a unique number. This unique number is used to resolve conflicts between log files. This ensures that each generated log files during runtime is unique to one another.
<handler-name>.formatter specifies the name of a Formatter class to use. The default value of this property is "java.util.logging.XMLFormatter". That's why the result in the example above in file is in XML format. More information about FileHandler properties can be found in the
documentation.
If you just want to instantiate a handler, just use their default constructors. For example:
//Instantiate default ConsoleHandler
ConsoleHandler ch = new ConsoleHandler();
//Instantiate default FileHandler
FileHandler fh = new FileHandler();
Custom Filter
By default, loggers and handlers don't have filters. However, we can create our custom filter that filters loggers and handlers. This example demonstrates adding filter to a logger.
import java.util.logging.*;
public class SampleClass{
static Logger logger =
Logger.getLogger("MyLogger");
public static void main(String[] args){
logger.setFilter(new MyFilter());
logger.log(Level.INFO, "Good Line1");
logger.log(Level.INFO, "Bad Line2");
logger.log(Level.INFO, "Bad Line3");
logger.log(Level.INFO, "Good Line4");
logger.log(Level.INFO, "Bad Line5");
}
}
class MyFilter implements Filter{
@Override
public boolean isLoggable(LogRecord record){
return record.getMessage().startsWith("Good");
}
}
Result
May 20, 2022 12:14:05 AM SampleClass main
INFO: Good Line1
May 20, 2022 12:14:05 AM SampleClass main
INFO: Good Line4
Take note that a filter can be applied to a logger, handler or both. If a filter is applied to a logger, logs are gonna be filtered before sending them to a handler. If a filter is applied to a handler, received logs are gonna be filtered before sending them to a console, file, etc.
Log Entry and Return of Methods
There are two convenient methods that we can use to log method entry and return. These methods are
entering
and
exiting
methods from Logger class. These methods have two overloaded forms. This example demonstrates their overloaded forms with two parameters.
import java.util.logging.*;
public class SampleClass{
private static final String className =
SampleClass.class.getName();
static Logger logger =
Logger.getLogger
(className);
public static void main(String[] args){
ConsoleHandler ch = new ConsoleHandler();
logger.setLevel(Level.FINER);
ch.setLevel(Level.FINER);
logger.addHandler(ch);
ClassA ca = new ClassA();
logger.entering(className, "main");
ca.classAMeth();
logger.exiting(className, "main");
}
}
class ClassA{
private final String className =
ClassA.class.getName();
Logger logger =
Logger.getLogger(className);
ClassA(){
ConsoleHandler ch = new ConsoleHandler();
logger.setLevel(Level.FINER);
ch.setLevel(Level.FINER);
logger.addHandler(ch);
}
void classAMeth(){
logger.entering(className, "classAMeth");
System.out.println("Class A Method!");
logger.exiting(className, "classAMeth");
}
}
Result
May 20, 2022 1:22:42 AM SampleClass main
FINER: ENTRY
May 20, 2022 1:22:42 AM SampleClass classAMeth
FINER: ENTRY
Class A Method!
May 20, 2022 1:22:42 AM SampleClass classAMeth
FINER: RETURN
May 20, 2022 1:22:42 AM SampleClass main
FINER: RETURN
In the example above, I logged the pre-entry and entry of execution in
classAMeth
; pre-return and return of execution from
classAMeth
and
main
. Take note that the logs of these two methods are loggable in FINER level. Thus, your logger and handler must be in FINER level or level lower than FINER.
Other forms of these two methods can be found in the
documentation.