Java Tutorials - Logger
Trace technical data, how to implement a logging feature in Java?
Trace technical data, how to implement a logging feature in Java?
This tutorial will show you how to handle a classical technical feature : the Log
(or
Logger
) feature
The class should be named IamLog
and should have 5 methods:
This exercise goal is to make you program an IamLog
Class, which is an
utility class,
allowing to write and keep technical information while the program is executing
When you will be in your first job, the log files are very important, as they are most of the time the only way to find what happened on the customer installation.
Your company product, an IAM software, is deployed on several customers installations. One day, one of your customer is calling your company's support to report that your application unexpectedly crashed, doing a particular action
This case has never been encountered by the support team, and they delegate this bug resolution to you
Fortunately, you added a Logging feature to your application, meaning that all important or critical operations made during the application execution are reported in a text file
Your log file content may look like this
2014/05/31 - 10:06:14.795 : fr.tbr.iamcore.Main.main()[19] : [INFO] Beginning of the program
2014/05/31 - 10:06:14.797 : fr.tbr.iamcore.Main.main()[21] : [INFO] proposing the main menu
2014/05/31 - 10:06:19.180 : fr.tbr.iamcore.Main.main()[30] : [SEVERE] got an exception, unable to continue
2014/05/31 - 10:06:19.180 : fr.tbr.iamcore.Main.main()[31] : [SEVERE] null
Thanks to this technical information, you know that the bug is located in the Main
class, in the main()
,
between the lines 21 and 30
So you open your code :
public static void main(String[] args) {
IamLog log = IamLog.getLogger(Main.class);
System.out.println("Welcome in the IAM System");
log.info("Beginning of the program");
Scanner scanner = new Scanner(System.in);
log.info("proposing the main menu"); //line 21
System.out.println("Please choose among the propositions");
System.out.println("Create a user (1), Modify a user (2), Delete a user (3)? Your choice (1|2|3)?");
int answer;
try {
answer = scanner.nextInt(); // user action
log.info("got answer: " + answer);
} catch (Exception e) {
log.error("got an exception, unable to continue"); //line 30
log.error(e.getMessage());
return;
}
log.info("continue with choice: " + answer);
}
You suddenly remember how you managed to output the log information: you did it through the IamLog
object. You created one local variable named "log" and called the info()
and error()
methods with the appropriate parameters. It has nothing with the user dialog, and
it does not
appear on his console, but is only reported in the log text file sent by the customer
Levels of log, such as "info" or "errror" allow to filter what is output in the log file. As an example, on production systems, as the load is heavy for the system running the application, the customer may want to disable any log, except critical (error) messages. This can be done thanks to the use of different methods, indicating whether a message is really important or not
And you notice now, that this can happen only if the user input is not satisfying the
nextInt()
contract, the input has to be an int
So you assume that the input was not correct, and ask for a screenshot of what he did to encounter the error.
What the customers send is below
The user did type a wrong option!
This exercise will make you apply several concepts, seen (or partially seen) during lectures, such as:
java.util.logging
featuresAs a first try for this feature implementation, we will stay with the most basic information, the date and time, and the message for this trace should be enough
As seen on this lecture, we have to
manipulate date and time through the type Date
In Java, we've seen that almost everything was Object. This is why anything we will do will begin by the creation of a Java class
Since the beginning of this course, we have seen two kinds of Java Class
Authenticator
class
So we have our entry point: the Main
class (at least with a main method). For the
project you have to realize, this Main class
will be, through the main method, the
place to call the
other objects for the application
For now we have made only one object before, the Authenticator
object. Remember?
package fr.tbr.iamcore.authentication;
/**
* This class allows to perform an authentication for a user trying to access the application
* {@link #authenticate(String, String)} for further detail about the authentication feature
* @author Tom
*/
public class Authenticator {
private String login;
private boolean authenticated;
/**
* This method allows to check if the user is granted according to its couple (login/pwd)
* @param userLogin the input login
* @param userPassword the input password
* @return true if the user is authenticated
*/
public boolean authenticate(String userLogin, String userPassword){
String foundPassword = getUserPassword(userLogin);
if (foundPassword == null || foundPassword.trim().isEmpty()){
//user was not found or no way to find a password
return false;
}
authenticated = userPassword.equals(foundPassword) ;
if (authenticated){
System.out.println("Access is granted !");
this.login = userLogin; //stores the login for further use
}else{
System.out.println("Access is denied ...");
}
return authenticated;
}
public String getLogin() {
return login;
}
public boolean isAuthenticated() {
return authenticated;
}
private String getUserPassword(String user){
//Should be implemented later, it should check against a database for instance or in a file.
return user;
}
}
Notice that there is no entry point, so to use this object in the program, you'll have to use it in a class holding a main method, such as the following example:
package fr.tbr.iamcore;
import java.util.Scanner;
import fr.tbr.iamcore.authentication.Authenticator;
/**
* The main entry point of the identity program
* @author Tom
*/
public class Main {
public static void main(String[] args) {
System.out.println("Welcome in the IAM System");
Scanner scanner = new Scanner(System.in);
System.out.println("Please authenticate, type your login :");
String readLogin = scanner.next();
System.out.println("type your password :");
String readPassword = scanner.next();
//Notice the Authenticator usage
Authenticator authenticator = new Authenticator();
authenticator.authenticate(readLogin, readPassword);
if (!authenticator.isAuthenticated()){
System.out.println("Invalid user/password combination, exiting...");
}
System.out.println("Successfully authenticated");
System.out.println("Please choose among the propositions");
System.out.println("Create a user (1), Modify a user (2), Delete a user (3)? Your choice (1|2|3)?");
int answer;
try {
answer = scanner.nextInt();
} catch (Exception e) {
return;
}
scanner.close();
}
}
This should have warmed you up about how to design and manipulate objects in Java.
As specified by the Exercise description, the IamLog class has to have 5 methods, according to this
description, we can draw this UML
schema
We can now create the IamLog
class
package fr.tbr.iamcore.logging;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* The logger for the IAM Log
*
* @author Tom
*/
public class IamLog {
static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd - HH:mm:ss.SSS");
private String loggingEntity;
/**
* Creates an instance of the logger with the given parameter as a
* "logging entity".
*
* @param loggingEntity
*/
public IamLog(String loggingEntity) {
this.loggingEntity = loggingEntity;
}
/**
* Output a log entry composed of the message and the level parameter
* The call log("Hello all", "INFO");
* will output: {the current date} - [INFO] - Hello all
*
* @param message
* the message you want to output
* @param level
* the importance of the message
*/
public void log(String message, String level) {
String trace = sdf.format(new Date()) + " " + loggingEntity + " - [" + level + "] - " + message;
System.out.println(trace);
}
/**
* output an INFO message
*
* @param message
* the message to record
*/
public void info(String message) {
log(message, "INFO");
}
/**
* output an WARN message
*
* @param message
* the message to record
*/
public void warn(String message) {
log(message, "WARN");
}
/**
* output an ERROR message
*
* @param message
* the message to record
*/
public void error(String message) {
log(message, "ERROR");
}
/**
* output a DEBUG message
*
* @param message
* the message to record
*/
public void debug(String message) {
log(message, "DEBUG");
}
}
Main
class, and use it as shown in the first part.package fr.tbr.iamcore;
import fr.tbr.iamcore.logging.IamLog;
public class Main {
public static void main(String[] args) {
//...
IamLog logger = new IamLog(Main.class.getSimpleName());
logger.debug("a sample debug test");
//... next code
}
}
2014/06/07 - 19:46:14.779 Main - [DEBUG] - a sample debug test
Outputing stuffs in the standard output - where the program will interact with the user - is not very convenient. So we need to have a more proper way to trace technical informations
This is why, our first improvement will be to write all these traces into a File
We've seen that the fact to log directly in the console output is not the good way to proceed, especially if your program is a console application
We will need the File
object, defined just below
File
, FileWriter
, FileReader
APIsThe Java Development Kit has a built in File management feature, provided by the File
object
The File
object is like a pointer on the concrete file you are attempting to
access. It provides useful method to allow to test if a file exists, to create it if not etc.
The File
object can be created using a path as a constructor, see the following example
File loggingFile = new File("C:/my/path/to/file");
To gain write and read access to this file you are forced to use respectively a FileWriter
and a
FileReader
. These objects take a File
as a parameter
FileWriter writer = new FileWriter(loggingFile);
writer.write("Created an IamLog instance, beginning of the log file");
For the reader, it is often useful to use a scanner, so you have convenient methods to read the file (next()
)
FileReader reader = new FileReader(loggingFile);
Scanner scan = new Scanner(reader);
String firstLine = scan.next();
Taking the previous hint in consideration, you can now improve your IamLog
by implanting a file
management in the constructor, this way:
public IamLog(String loggingEntity) {
File loggingFile = new File("C:/my/path/to/file");
if (!loggingFile.exists()){ // test if the file is really existing on the file system
try{
//try to create the file if it does not exist
loggingFile.createNewFile();
}catch(IOException ioe){
//IOException is the standard exception when you have problem while accessing
//to a physical system resource
System.out.println("An error occurred while preparing the log file");
}
}
try {
this.writer = new FileWriter(loggingFile);
this.writer.write("Created an IamLog instance, beginning of the log file");
this.writer.flush(); concretely outputting the string in the file
} catch (IOException e) {
//Could not write in the file
}
this.loggingEntity = loggingEntity;
}
Then you can improve the log()
method by replacing the console output with the file one
public void log(String message, String level) {
String trace = sdf.format(new Date()) + " " + loggingEntity + " - [" + level + "] - " + message;
//old call : System.out.println(trace);
//new call:
try {
this.writer.write(trace);
this.writer.flush(); // this will concretely output the string in the file
} catch (IOException e) {
e.printStackTrace(); //Handle exception here
}
}
After the first run, we obtain that in the targeted log file
Created an IamLog instance, beginning of the log file2014/06/09 - 11:52:10.729 Main - [DEBUG] - a sample debug test
It lacks line feed... Indeed, the FileWriter provides no writeln()
or println()
method. So
you have two choices, you can improve the writer by wrapping around it a PrintWriter
, or you can
concatenate the standard "line separator" to the String end.
You have to replace the type of writer
by PrintWriter
, which will give you access to useful
methods like println()
//... extract of the IamLog constructor
try {
this.writer = new PrintWriter(new FileWriter(loggingFile));
writer.println("Created an IamLog instance, beginning of the log file");
writer.flush();
} catch (IOException e) {
//Could not write in the file
}
//the log method
public void log(String message, String level) {
String trace = sdf.format(new Date()) + " " + loggingEntity + " - [" + level + "] - " + message;
//old call : System.out.println(trace);
//new call:
this.writer.println(trace);
writer.flush();
}
//...
//Just a hint : If you want to get the standard line separator:
System.getProperty("line.separator");
//...
This provides the following output now:
Created an IamLog instance, beginning of the log file
2014/06/09 - 12:10:19.592 Main - [DEBUG] - a sample debug test
In the log method, you can provide whatever you want as a second parameter, there is no constraint on the
level
parameter
Example
These calls
logger.log("a sample", "INFO");
logger.log("a bad sample", "BlahBlah");
Will produce
2014/06/09 - 12:10:19.592 Main - [INFO] - a sample
2014/06/09 - 12:10:19.592 Main - [BlahBlah] - a bad sample
This is not good, because it means every developer can have his way to output logs.To avoid that, we can use two possibilities :
To define constants, you have to use a combination of both static final
keywords, if you want to make
them public, simply add public
, just as the following block of code
public class IamLog {
public static final String INFO = "INFO";
public static final String WARN = "WARN";
public static final String DEBUG = "DEBUG";
public static final String ERROR = "ERROR";
//...
}
To use them, just proceed as follow
logger.log("a sample", IamLog.INFO);
This is a bit better, but you still cannot avoid to use a random String, to achieve that functionality, we will need
the Enumeration
feature
An enumeration is a predefine "enumeration"(say a list) of constants, this is a Java entity so it can be considered as a very particular class.
As a result of this remark, an enumeration constitutes a type
The enumeration can be provided in its own file, or in an existing class. This is this last solution that will be chosen:
public class IamLog {
// public static final String INFO = "INFO";
// public static final String WARN = "WARN";
// public static final String DEBUG = "DEBUG";
// public static final String ERROR = "ERROR";
/**
* Levels for the logging feature
* @author Tom
*/
public enum Level{
INFO,
WARN,
DEBUG,
ERROR
}
//...
//Notice the modification in the log method, the second parameter is of type "Level",
//instead of String
public void log(String message, Level level) {
//...
}
And the usage is as specified below
logger.log("a sample", Level.INFO);
logger.log("a good sample", Level.WARN);
If you try to put something else (a raw String for instance, like "BlahBlah"), you will now have a compilation error
The JDK embeds a ready-to-use logger feature, the tutorial will soon include the description of how to use it. You still have a working sample in the source code below