System.Logger

Log from any Java code with the built-in System.Logger API, and know where its output goes and which message-format traps to avoid.

SyntaxSystem.Logger log = System.getLogger("com.shop.orders"); log.log(System.Logger.Level.INFO, "Order {0} placed", id); log.log(System.Logger.Level.DEBUG, () -> expensiveText()); // lazy log.log(System.Logger.Level.ERROR, "Payment failed", ex);

System.Logger is the logging API built into java.base since Java 9. You get one with System.getLogger(name) and call log(level, message) on it. It is a front end only: the records are passed to a backend that decides the format and where they go. By default that backend is java.util.logging (JUL) when the java.logging module is present, and a logging library can replace it by supplying a System.LoggerFinder.

Why it exists

A library that logs should not force a logging framework on the application using it. Code written against System.Logger has no dependency at all, and the application picks the backend. SLF4J, for example, ships a bridge that routes System.Logger calls into Logback. For a small service or a command-line tool with no framework, it is a perfectly good logger on its own.

private static final System.Logger LOG = System.getLogger(OrderService.class.getName());

LOG.log(System.Logger.Level.INFO, "Order {0} placed for {1}", orderId, customer);
LOG.log(System.Logger.Level.DEBUG, () -> "Basket: " + basket.describe());
LOG.log(System.Logger.Level.ERROR, "Payment failed", exception);

Name loggers after the class (or its package). The name is what lets you raise or lower the level for one part of the service later.

Where the output goes

With the default JUL configuration, records at INFO and above go to standard error as two lines: a timestamp with the class and method, then the level and message. DEBUG and TRACE are dropped. The programs on this page install a tiny handler that prints one line to standard output instead, so the output can be shown exactly. In a real service you configure the backend once at startup, not in each class.

Message formats

The log(level, format, params...) overload uses java.text.MessageFormat, and that causes the classic mistakes:

  • {} is not a placeholder. That is SLF4J syntax. Here it is printed literally; use {0}, {1}.
  • Numbers are formatted for the locale. An order id of 12345 prints as 12,345, which breaks every log search for that id. Pass ids as strings.
  • A single quote starts a quoted section. "Can't find order {0}" prints Cant find order {0}. Write '' for an apostrophe.

Prefer the Supplier overload for messages that cost something to build: the lambda only runs when the level is enabled. Pass exceptions with the Throwable overload rather than concatenating e.getMessage(), so the backend can record the stack trace.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Add logging to a shared internal library without forcing a logging framework on every service that depends on it.
  • Log startup, shutdown and request failures in a small JDK-only HTTP service built on com.sun.net.httpserver.
  • Route a library's System.Logger output into the application's Logback setup through the SLF4J bridge so all logs share one format.

More examples

Three MessageFormat traps

SLF4J-style braces are ignored, an int id gains a grouping comma, and a lone apostrophe quotes away the placeholder. Passing the id as a string and doubling the apostrophe gives the line you meant.

Example · java
import java.util.logging.*;

// Demo setup: print each record to stdout as "LEVEL logger - message".
void logToStdout() {
    Logger root = Logger.getLogger("");
    for (Handler h : root.getHandlers()) root.removeHandler(h);
    Formatter fmt = new SimpleFormatter();
    root.addHandler(new Handler() {
        @Override public void publish(LogRecord r) {
            if (isLoggable(r)) IO.println(r.getLevel() + " " + r.getLoggerName() + " - " + fmt.formatMessage(r));
        }
        @Override public void flush() {}
        @Override public void close() {}
    });
}

void main() {
    logToStdout();
    System.Logger log = System.getLogger("api");
    log.log(System.Logger.Level.INFO, "Order {} shipped", 12345);
    log.log(System.Logger.Level.INFO, "Order {0} shipped", 12345);
    log.log(System.Logger.Level.INFO, "Can't find order {0}", "12345");
    log.log(System.Logger.Level.INFO, "Can''t find order {0}", "12345");
}
// Output:
// INFO api - Order {} shipped
// INFO api - Order 12,345 shipped
// INFO api - Cant find order {0}
// INFO api - Can't find order 12345

Lazy messages with a Supplier

DEBUG is off by default, so the thousand lambdas never run and nothing is built. The INFO call runs its supplier once because that level is enabled.

Example · java
import java.util.logging.*;

// Demo setup: print each record to stdout as "LEVEL logger - message".
void logToStdout() {
    Logger root = Logger.getLogger("");
    for (Handler h : root.getHandlers()) root.removeHandler(h);
    Formatter fmt = new SimpleFormatter();
    root.addHandler(new Handler() {
        @Override public void publish(LogRecord r) {
            if (isLoggable(r)) IO.println(r.getLevel() + " " + r.getLoggerName() + " - " + fmt.formatMessage(r));
        }
        @Override public void flush() {}
        @Override public void close() {}
    });
}

int renders = 0;

String describe(List<String> basket) {
    renders++;
    return String.join(", ", basket);
}

void main() {
    logToStdout();
    System.Logger log = System.getLogger("basket");
    List<String> basket = List.of("tea", "milk", "bread");

    for (int i = 0; i < 1000; i++) {
        log.log(System.Logger.Level.DEBUG, () -> "Basket: " + describe(basket));
    }
    IO.println("DEBUG enabled: " + log.isLoggable(System.Logger.Level.DEBUG));
    IO.println("Messages built: " + renders);

    log.log(System.Logger.Level.INFO, () -> "Basket: " + describe(basket));
    IO.println("Messages built: " + renders);
}
// Output:
// DEBUG enabled: false
// Messages built: 0
// INFO basket - Basket: tea, milk, bread
// Messages built: 1

Logging an exception once, at the boundary

The Throwable overload attaches the exception to the record, so a real backend prints its stack trace; System.Logger ERROR arrives in JUL as SEVERE. The payment client throws and only the handler that turns the failure into a status code logs it.

Example · java
import java.util.logging.*;

// Demo setup: show the level, message and the attached exception.
void logToStdout() {
    Logger root = Logger.getLogger("");
    for (Handler h : root.getHandlers()) root.removeHandler(h);
    root.addHandler(new Handler() {
        @Override public void publish(LogRecord r) {
            String cause = r.getThrown() == null ? "" : " [" + r.getThrown().getClass().getSimpleName()
                    + ": " + r.getThrown().getMessage() + "]";
            IO.println(r.getLevel() + " " + r.getMessage() + cause);
        }
        @Override public void flush() {}
        @Override public void close() {}
    });
}

class PaymentClient {
    void charge(String orderId) {
        throw new IllegalStateException("card declined for " + orderId);
    }
}

class CheckoutHandler {
    private static final System.Logger LOG = System.getLogger("checkout");
    private final PaymentClient payments = new PaymentClient();

    int handle(String orderId) {
        try {
            payments.charge(orderId);
            return 200;
        } catch (IllegalStateException e) {
            LOG.log(System.Logger.Level.ERROR, "Checkout failed", e);
            return 502;
        }
    }
}

void main() {
    logToStdout();
    IO.println("status " + new CheckoutHandler().handle("A-7"));
}
// Output:
// SEVERE Checkout failed [IllegalStateException: card declined for A-7]
// status 502

Discussion

  • Be the first to comment on this lesson.