Lesson 66Phase 8: Tools of the trade
Logging instead of println
Recording what a program does with levels and loggers, using java.util.logging and SLF4J with Logback.
By BasithUpdated 35 min
- logging
- Logger
- levels
- SLF4J
- Logback
- parameterized messages
Every program in this course talks to you through System.out.println. When something goes wrong, you add a few more printlns to see what is happening, and delete them once the bug is gone. That works for a program you run once. A real program runs for days on a machine you are not watching, and when it misbehaves you need a record of what it did, when, and where. This lesson swaps the stray printlns for logging: messages with a level, a time and a source, that you can turn up or down without touching the code.
- Say what println gets wrong as a record of what a program did, and what a logger adds.
- Create a
java.util.logging.Logger, log at the right level, and read the record it prints. - Explain why a FINE message can stay hidden after you set the logger to FINE, and fix it.
- Write messages with parameters and with a Supplier, and log an exception with its stack trace.
- Use SLF4J with Logback in a Maven project, and configure it with a
logback.xmlfile. - Choose a level for a message, and name what must never go into a log.
private static final field, from static and final. Lazy messages are Supplier lambdas, from enums and lambdas. And a logged exception prints the stack trace you learned to read in annotations and tooling.What println gets wrong#
Picture a shop app that has been running on a server for a week. A customer says their order vanished on Tuesday afternoon. You open the program's output and find thousands of lines like adding 250 and here!!, with no times, no idea which part of the program printed them, and the real output mixed in between. Debugging printlns cause each of those problems:
java.util.logging: the logger inside the JDK#
Java ships with a logging library in the package java.util.logging, so you can start without adding anything. Each class gets its own Logger, usually named after the class, kept in a private static final field. Run this and look at the console: the log lines appear in red, because they go to standard error, and Welcome!is the program's real output.
import java.util.logging.Logger; public class Main { private static final Logger log = Logger.getLogger(Main.class.getName()); public static void main(String[] args) { log.info("Opening the shop"); int stock = 2; if (stock < 3) { log.warning("Stock is low: " + stock + " left"); } log.fine("Checked the stock"); System.out.println("Welcome!"); }}Each message becomes a log record of two lines. The first has the date and time, then the class and the method that logged it, Main main. The second has the level and the message, INFO: Opening the shop. Main.class.getName()is the class's full name, so in a class inside a package the logger would be called something like com.example.shop.Main. And the fine message printed nothing at all. That is levels at work.
Seven levels#
| Level | Method | Use it for |
|---|---|---|
SEVERE | log.severe(...) | Something failed and a person needs to know: a payment did not go through, the database is unreachable. |
WARNING | log.warning(...) | Something unexpected that the program handled: stock is low, a setting was missing and the default was used. |
INFO | log.info(...) | The milestones of a normal run: the shop opened, 1,234 books loaded, an order was placed. |
CONFIG | log.config(...) | The settings the program started with. |
FINE, FINER, FINEST | log.fine(...) | Detail for developers chasing a bug, from fine to finest. Hidden normally. |
A logger prints a message only if its level is at least the logger's own level. Out of the box that is INFO, so SEVERE, WARNING and INFO appear and everything below is dropped. The dropped messages cost almost nothing, which is why they can stay in the code.
The two gates: logger and handler#
To see the FINE messages while you hunt a bug, the obvious move is log.setLevel(Level.FINE). Try it, and watch the first fine call vanish anyway:
import java.util.logging.ConsoleHandler;import java.util.logging.Level;import java.util.logging.Logger; public class Main { private static final Logger log = Logger.getLogger("shop"); public static void main(String[] args) { log.setLevel(Level.FINE); log.fine("1: the logger lets FINE in, but the handler does not"); ConsoleHandler handler = new ConsoleHandler(); handler.setLevel(Level.FINE); log.addHandler(handler); log.setUseParentHandlers(false); log.fine("2: now both gates let FINE through"); log.finer("3: FINER is below the logger's level"); }}Only message 2 is printed. A record passes two gates. The logger decides whether a message is worth recording at all; a handler decides whether to write it out, and where. The handler that prints to the console belongs to the root logger, the parent of every logger, and it has its own level, also INFO. Message 1 got through the first gate and was stopped at the second. The fix gives the logger its own ConsoleHandler set to FINE, and setUseParentHandlers(false)stops the record from also going to the root's handler, which would print it a second time once it allowed FINE.
Key ideaA log record is printed only if the logger's level and the handler's level both let it through. Raising one of them alone changes nothing.
Messages that cost nothing when they are dropped#
Building a message has a cost. log.fine("Cart: " + cart) joins the strings and calls cart.toString() before fine even starts, and then the record is dropped. Two forms avoid the waste. A message with numbered placeholders, {0}, {1}, is only filled in if the record is printed. A Supplier lambda is only called if the record is printed. Step through and watch the counter.
import java.util.logging.Level;import java.util.logging.Logger; public class Main { private static final Logger log = Logger.getLogger("shop"); static int calls = 0; public static void main(String[] args) { int books = 1234; log.log(Level.INFO, "Loaded {0} books from {1}", new Object[]{books, "library.txt"}); log.log(Level.INFO, "Average price {0}", 12.3456); log.fine(() -> "Report: " + slowReport()); System.out.println("slowReport ran " + calls + " times"); } static String slowReport() { calls++; return "lots of detail"; }}The log says Loaded 1,234 books from library.txt and Average price 12.346, and the output says slowReport ran 0 times: the fine record was dropped, so its lambda never ran. The placeholders are filled by java.text.MessageFormat, which formats numbers for reading. It adds the comma in 1,234 and rounds the price to three decimal places.
MessageFormat, a single quote starts a piece of text to copy as it is. So log.log(Level.INFO, "Can't find {0}", "Dune") logs Cant find {0}: the quote vanished and so did Dune. Write two single quotes, Can''t find {0}, to get one. (A message with no parameters is printed exactly as written, quote and all.)import java.util.logging.Logger; public class Main { private static final Logger log = Logger.getLogger("shop"); static int calls = 0; public static void main(String[] args) { log.fine("A: " + report()); log.fine(() -> "B: " + report()); log.info(() -> "C: " + report()); System.out.println(calls); } static String report() { calls++; return "details"; }}The logger is at the default level, INFO. How many times does report() run?
Twice. Line A builds its string, calling report(), before finecan say the record is dropped. Line B hands over a lambda that is never called. Line C's lambda is called, because INFO is printed.
Logging an exception#
When you catch an exception and carry on, log it with the exception as the last argument. The record then includes the full stack trace, so the log answers “where did this come from?” long after the program has moved on.
import java.util.logging.Level;import java.util.logging.Logger; public class Main { private static final Logger log = Logger.getLogger("shop"); public static void main(String[] args) { int[] prices = {450, -200, 320}; int total = 0; for (int p : prices) { try { total += checked(p); } catch (IllegalArgumentException e) { log.log(Level.WARNING, "Skipping a bad price", e); } } System.out.println("Total: " + total); } static int checked(int price) { if (price < 0) { throw new IllegalArgumentException("price cannot be negative: " + price); } return price; }}The program prints Total: 770 and logs a WARNING with the exception and its trace under it: at Main.checked(Main.java:22), then at Main.main(Main.java:12). Logging e.getMessage() alone would lose those lines, and they are the most useful part.
System.Logger: the JDK's front door#
Java 9 added a smaller logging interface, System.Logger, that you get from System.getLogger(name). It has one method, log, with its own five levels, and it hands every record to a logging library: java.util.loggingby default, or another one, such as SLF4J, when that library's bridge for System.Logger is on the classpath (for SLF4J, the slf4j-jdk-platform-logging module). Code written against it never has to change when the library does.
public class Main { private static final System.Logger log = System.getLogger("shop"); public static void main(String[] args) { log.log(System.Logger.Level.INFO, "Opening the shop"); log.log(System.Logger.Level.WARNING, "Only {0} copies of {1} left", 2, "Dune"); log.log(System.Logger.Level.DEBUG, "Hidden: DEBUG means FINE underneath"); log.log(System.Logger.Level.ERROR, "The till is broken"); }}Three records appear. DEBUG becomes FINE in java.util.logging, which is dropped at the default level, and ERROR is printed as SEVERE, the name java.util.logging uses. The parameters work the same way, through MessageFormat.
SLF4J and Logback: what most projects use#
Look inside almost any Java project from a company and you will find a different pair: SLF4J (the Simple Logging Facade for Java) and Logback. They split the job in two, the way an interface and its implementation do in interfaces. SLF4J is the interface your code calls. Logback is the library that does the work: formatting, writing to the console and to files. A team can swap Logback for another implementation without changing a line of the code that logs.
They come from Maven Central like any library. Adding Logback is enough, because it brings the SLF4J interface with it:
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.6.5</version></dependency>package com.example.shop; import org.slf4j.Logger;import org.slf4j.LoggerFactory; public class Main { private static final Logger log = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { log.info("Opening the shop"); int books = 1234; String file = "library.txt"; log.info("Loaded {} books from {}", books, file); log.debug("Checking stock levels"); log.trace("Too detailed to show"); log.warn("Only {} copies of {} left", 2, "Dune"); try { throw new IllegalStateException("drawer stuck"); } catch (IllegalStateException e) { log.error("Could not open the till", e); } }}It reads much like java.util.logging, with three differences you can see. The levels are trace, debug, info, warn and error. Placeholders are plain {}, filled in order, and only when the record is printed. And an exception goes last, after the placeholders' values. Run it with the Exec plugin:
$ mvn compile exec:java -Dexec.mainClass=com.example.shop.Main...[INFO] --- exec:3.6.4:java (default-cli) @ shop ---15:34:50.736 [com.example.shop.Main.main()] INFO com.example.shop.Main -- Opening the shop15:34:50.736 [com.example.shop.Main.main()] INFO com.example.shop.Main -- Loaded 1234 books from library.txt15:34:50.736 [com.example.shop.Main.main()] DEBUG com.example.shop.Main -- Checking stock levels15:34:50.736 [com.example.shop.Main.main()] WARN com.example.shop.Main -- Only 2 copies of Dune left15:34:50.737 [com.example.shop.Main.main()] ERROR com.example.shop.Main -- Could not open the tilljava.lang.IllegalStateException: drawer stuck at com.example.shop.Main.main(Main.java:18) ...[INFO] BUILD SUCCESSWith no settings file, Logback prints one line per record: the time to the millisecond, the thread in brackets, the level, the logger's name, and the message. Its default level is DEBUG, so the debug line shows and the trace line does not. The thread is called com.example.shop.Main.main() because the Exec plugin starts the program on a thread of its own with that name; run it with java directly and it says main. The number is 1234, with no comma: SLF4J's {} uses plain toString, with none of MessageFormat's formatting and none of its quote trouble.
Configuring Logback with logback.xml#
Logback reads its settings from a file called logback.xml in src/main/resources, which Maven puts on the classpath. This one prints a shorter line to the console, writes a fuller one to a file called shop.log, keeps everything at INFO, and turns the shop's own code up to DEBUG:
<configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss} %-5level %logger{20} - %msg%n</pattern> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>shop.log</file> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern> </encoder> </appender> <logger name="com.example.shop" level="DEBUG"/> <root level="INFO"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root></configuration>15:35:12 INFO c.example.shop.Main - Opening the shop15:35:12 INFO c.example.shop.Main - Loaded 1234 books from library.txt15:35:12 DEBUG c.example.shop.Main - Checking stock levels15:35:12 WARN c.example.shop.Main - Only 2 copies of Dune left15:35:12 ERROR c.example.shop.Main - Could not open the tilljava.lang.IllegalStateException: drawer stuck at com.example.shop.Main.main(Main.java:18) ...- An appender is Logback's word for a destination, what
java.util.loggingcalls a handler. This file has two, the console andshop.log, and the root logger sends every record to both. The file holds the same records in the fuller pattern, with the date and the thread. - In the pattern,
%dis the date and time,%-5levelthe level padded to five characters so the messages line up,%logger{20}the logger's name shortened to about 20 characters (combecamec),%msgthe message and%na new line. <logger name="com.example.shop" level="DEBUG"/>sets one package's level. Loggers are named after classes, and a setting forcom.example.shopcovers every logger whose name starts with it. So your own code logs DEBUG while the libraries you use stay at INFO and stay quiet.
java.util.logging or System.Logger needs nothing extra. In a Maven or Gradle project, and in any project that uses Spring Boot, SLF4J with Logback is what you will meet, and what Spring Boot sets up for you.What to log, and what never to log#
A log is only as useful as what you put in it. A few habits make the difference between a log that answers questions and a log nobody reads.
- Log events, one record each, in a sentence a stranger could understand:
Order 5521 placed by customer 873 for 3 items, nothereorok. - Include the ids that let someone find the thing later: the order number, the customer number, the file name.
- Pick the level for the reader. If a person must act, it is ERROR (or SEVERE). If the program coped, WARN. If it is a normal milestone, INFO. Everything a developer wants only while debugging is DEBUG or below.
- Log an exception once, where you handle it, with the exception attached. Logging it and then throwing it again means the same error appears in the log two or three times as it travels up.
Try it
Answer each one in your head or on paper first, then open the answer.
log.setLevel(Level.FINEST) on their logger to see every detail, but still sees only INFO and above. What is going on, and what are two ways to fix it?log.fine(() -> "Cart: " + cart) better than log.fine("Cart: " + cart) when FINE is switched off?log.log(Level.INFO, "Don't sell {0}", "Dune") print with java.util.logging, and with SLF4J what does log.info("Don't sell {}", "Dune") print?IOException, logs it with log.error("Save failed", e), and then throws it again. Its caller does the same, and so does the caller's caller. What does the log look like, and what is the better habit?Practice
Work through these on your own. Each one builds on this page.
logging.properties file that shows FINE for your class only. Write down which messages appear each time.trace to error, and write a logback.xml that sends WARN and above to a file called problems.log while the console shows INFO and above.log.info("here"), log.info("user " + user) where user holds a name, email and password, log.error("oops") in a catch block, and log.info("i=" + i) inside a loop of a million items.Further reading#
Spotted a mistake, or a part that did not make sense? Report it by email