Java Module 14 – Testing, build tools and code quality
Debugging techniques and logging
Find Java bugs with conditional, exception and field breakpoints in jdb, log with System.Logger and java.util.logging, and keep secrets out of logs.
What you will learn
- Choose between a debugger and logs for a given bug, and debug with conditional, exception and field breakpoints
- Log with System.Logger and java.util.logging, and set levels, handlers and formats in logging.properties
- Write log lines that cost nothing when disabled and help operations without leaking secrets or personal data
Before you start
What you’ll learn · Before you start
What you will learn
- Choose between a debugger and logs for a given bug, and debug with conditional, exception and field breakpoints
- Log with System.Logger and java.util.logging, and set levels, handlers and formats in logging.properties
- Write log lines that cost nothing when disabled and help operations without leaking secrets or personal data
Before you start
On this page
Debugging techniques are the ways to learn why a program does what it does. The first step is always to
reproduce the failure. Then either watch it happen with a debugger, which pauses a running JVM at breakpoints, or read
what the program recorded about itself in its logs. A breakpoint can stop on a line, but also only when a condition
holds, when an exception is thrown or when a field changes. Logging records events while the program runs, at
levels from TRACE to ERROR, so that someone can understand a failure where no debugger can be attached, such as in
production. Java has a logging API of its own, System.Logger, which writes through java.util.logging unless a
library supplies another backend. Use the debugger when you can reproduce a bug, and logs for everything you cannot.
A log never contains passwords, tokens or personal data.
This lesson builds on the first debugging lesson, which set line breakpoints and stepped through code in an IDE and in jdb, and runs everything on Eclipse Temurin 25.0.4.1.
A debugger or logs
| Question | A debugger | Logs |
|---|---|---|
| What it shows | What the program is doing now, at one place, with every variable | What the program did, over time, everywhere it logs |
| What it needs | A reproduction, and a JVM that you may pause | Nothing more, once the log lines are in the code |
| In production | Rarely: a pause stops real work, and a debug port controls the JVM | Yes: that is what logs are for |
| What it costs | Every stop is a round trip between two JVMs | A disabled line costs almost nothing when written well |
| What can go wrong | A debug port reachable from a network | Secrets and personal data copied into log files |
The two meet in the middle. A good log line, added while debugging, keeps answering the same question after the bug is fixed. Both debugging techniques and logging work best with a precise question, such as “which order made the total negative?”, rather than “what is wrong?”.
Breakpoints beyond the line, by name
- A conditional breakpoint carries a condition, such as
amount < 0, and pauses only on the passes where it holds, so a loop of 10,000 rounds shows you the one that matters. - An exception breakpoint stops where an exception is thrown, before any
catchor stack unwinding hides where it came from; jdb calls itcatch. - A watchpoint stops when a field is read or changed, which finds the line that changed it; jdb calls it
watch. - A logpoint in an IDE prints a message when the line runs and does not stop at all.
- Remote debugging attaches a debugger to a JVM started with the JDWP agent,
-agentlib:jdwp=…, for example on a test server. - A thread dump, from
jcmd <pid> Thread.print, shows every thread’s stack at one moment without stopping the program.
Here is a bug hunt with a conditional breakpoint, step by step. A report of 10,000 orders shows a total that is 54,921
paise lower than it should be. A line breakpoint on total += amount would stop 10,000 times. Ask what a wrong total
needs: an amount that cannot be right. So set the breakpoint’s condition to amount < 0, run, and the program stops
once, with i telling you which order it was. From there, step back to where amount was computed, and the cause is
on the screen. If no condition comes to mind, an exception breakpoint or a watchpoint on the suspicious field usually
narrows the search the same way.
Logging with System.Logger and java.util.logging
The path of one log record, and the two levels it must pass
Text description of the diagram
The diagram shows the path of one log record from top to bottom.
- Your code calls LOG.log(Level.DEBUG, ...) on a System.Logger that it got from System.getLogger(name).
- The JDK's default System.LoggerFinder hands the record to java.util.logging, and DEBUG becomes the java.util.logging level FINE.
- The java.util.logging Logger is the first gate: the record goes on only if its level is at or above the logger's level, or the level the logger inherits from its parents.
- The ConsoleHandler is the second gate: it has a level of its own, INFO by default, and drops records below it.
- The SimpleFormatter turns the record into one line, in the format that logging.properties sets.
- The line is written to standard error.
A dashed box, logging.properties, has dashed arrows to the logger and to the handler: that file sets the levels of both, the handlers and the format.
payments/src/com/example/payments/Settlement.java
package com.example.payments;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
/// Settles one day of a shop's UPI payments and logs what it does, at every level that System.Logger has.
public class Settlement {
private static final Logger LOG = System.getLogger(Settlement.class.getName());
public static void main(String[] args) {
long[] payments = {49_900, 125_000, 9_900, 310_000};
long total = 0;
for (long paise : payments) {
LOG.log(Level.TRACE, () -> "adding a payment of " + paise + " paise");
total += paise;
}
LOG.log(Level.DEBUG, "total of {0} payments: {1} paise", payments.length, total);
LOG.log(Level.INFO, "settled {0} paise for shop {1}", total, "S-1042");
LOG.log(Level.WARNING, "the bank answered after {0} ms; the limit is {1} ms", 2_400, 2_000);
LOG.log(Level.ERROR, "refund of payment 3 failed", new IllegalStateException("account closed"));
}
} payments/logging.properties
# java.util.logging settings, read when the JVM starts with -Djava.util.logging.config.file=logging.properties.
# One handler: the console, which java.util.logging writes to standard error. It passes on every record that the
# loggers let through.
handlers = java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = ALL
# Level, logger name, message and the exception, if any. No date or time: a platform that collects the console of
# a service adds its own.
java.util.logging.SimpleFormatter.format = %4$-7s %3$s: %5$s%6$s%n
# Every logger: INFO and above.
.level = INFO payments/logging-debug.properties
# logging.properties, plus one line: the loggers of the payment code also let FINE through, which is System.Logger's
# DEBUG. All other loggers stay at INFO.
handlers = java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.SimpleFormatter.format = %4$-7s %3$s: %5$s%6$s%n
.level = INFO
com.example.payments.level = FINE from session import run
# One day's settlement logged through System.Logger and java.util.logging: first with every logger at INFO, then
# with the payment code's loggers at FINE, which is System.Logger's DEBUG.
run(
"""
javac -d out payments/src/com/example/payments/Settlement.java
java -Djava.util.logging.config.file=payments/logging.properties -cp out com.example.payments.Settlement
java -Djava.util.logging.config.file=payments/logging-debug.properties -cp out com.example.payments.Settlement
"""
) Output
# openjdk version "25.0.4.1" LTS $ javac -d out payments/src/com/example/payments/Settlement.java $ java -Djava.util.logging.config.file=payments/logging.properties -cp out com.example.payments.Settlement INFO com.example.payments.Settlement: settled 494,800 paise for shop S-1042 WARNING com.example.payments.Settlement: the bank answered after 2,400 ms; the limit is 2,000 ms SEVERE com.example.payments.Settlement: refund of payment 3 failed java.lang.IllegalStateException: account closed at com.example.payments.Settlement.main(Settlement.java:20) $ java -Djava.util.logging.config.file=payments/logging-debug.properties -cp out com.example.payments.Settlement FINE com.example.payments.Settlement: total of 4 payments: 494,800 paise INFO com.example.payments.Settlement: settled 494,800 paise for shop S-1042 WARNING com.example.payments.Settlement: the bank answered after 2,400 ms; the limit is 2,000 ms SEVERE com.example.payments.Settlement: refund of payment 3 failed java.lang.IllegalStateException: account closed at com.example.payments.Settlement.main(Settlement.java:20)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 logging_session.py
System.getLogger(name) gave the class a logger named after it, and the JDK’s default System.LoggerFinder handed
its records to java.util.logging, which read logging.properties at start-up. With every logger at INFO, the first
run printed the INFO, WARNING and ERROR lines, the last one with its exception and stack trace. The second run let the
loggers of com.example.payments through at FINE and printed the DEBUG line as well. Neither run printed the TRACE
lines, and their message suppliers never ran. The {0} placeholders are filled in by java.text.MessageFormat, which
wrote 494,800 with the grouping of the run’s locale.
The levels printed are the names of java.util.logging, because that is the backend:
| System.Logger level | java.util.logging level | Use it for |
|---|---|---|
ERROR |
SEVERE |
Something failed and someone must act |
WARNING |
WARNING |
Something unusual that the program handled |
INFO |
INFO |
What the program did, at a rate that operations can read |
DEBUG |
FINE |
Detail for developers, off in production |
TRACE |
FINER |
Every step, off almost always |
The mapping is the one in the System.Logger.Level documentation. Logging set up like this costs one line per class
and a properties file per environment. Two more rules keep such logs safe to keep and to share: secrets and personal data are left
out or masked, and values that users typed are escaped, so that a line break in a name cannot forge a second record.
Check yourself
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Breakpoints beyond the line, measured: conditions, exceptions, watchpoints, attaching Pro: opens with a Pro, Premium or Ultimate pass
Every kind of breakpoint is the debugger asking the JVM to stop on some event, and every stop has a price. This part measures that price, uses exception breakpoints and watchpoints in jdb, and attaches to a JVM the way you would on a test server.
In this part:
- What a conditional breakpoint costs, measured
- Exception breakpoints and watchpoints in jdb, reproduced
- Attaching to a running JVM, and keeping the port closed
5 runnable examples (Java, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Logging that costs nothing when it is off, and levels a team can rely on Pro: opens with a Pro, Premium or Ultimate pass
A log line is code that runs on every pass, even when its level is switched off. This part measures what a disabled debug line costs in four spellings, reproduces the most common reason a DEBUG line never appears, and sets out what belongs at each level.
In this part:
- Four ways to write a debug line, measured
- Two gates, and the DEBUG line that never appears, reproduced
- A level policy for a payment service
2 runnable examples (Java, Python) · about 3 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Logs without secrets or forged lines: a leak found by a test, a forgery reproduced Pro: opens with a Pro, Premium or Ultimate pass
Logs are copied to more places than any database: other machines, log services, support tickets, screenshots. This part finds a leaked token with a test, lists what must never be logged, and reproduces a log line forged by a user.
In this part:
- A leaked token and payment address, found by a test
- What never goes into a log
- A forged log line, reproduced
- Exercise: fix the order service’s logging
6 runnable examples (ExerciseCheck.java, Java, LeakCheck.java, Order.java, OrderService.java, PaymentClient.java, PaymentClient.java, fixed, Python) · about 3 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
-
Fresher to mid · Warm-up
What is System.Logger, and how does it relate to java.util.logging?
System.Logger is the logging interface built into the JDK since Java 9: code gets a logger with System.getLogger(name) and logs at the levels TRACE, DEBUG, INFO, WARNING and ERROR, with plain messages, placeholders or a Supplier that builds the message only when the level is on. It is only the front. A System.LoggerFinder decides where records go, and the JDK's default finder uses java.util.logging as the backend whenever the java.logging module is present. Then DEBUG becomes FINE, TRACE becomes FINER and ERROR becomes SEVERE, and logging.properties sets the levels, handlers and format. A library such as SLF4J can install a finder of its own, so code that logs through System.Logger works with whatever backend the application chose.
-
Mid to senior
A batch job processes 2 million records in 3 seconds, and one record comes out wrong. A colleague sets a conditional breakpoint inside the loop, and the job now seems to hang. Why, and how do you find the bad record quickly?
-
Mid to senior
Support asks you to switch on DEBUG logging for the payment service in production for a day, to investigate failed payments. What do you check before you do it, how do you do it safely, and what can go wrong?
-
Mid to staff
The security team finds lines in a payment service's logs that look like real refund approvals, but the code never wrote them. How could that happen, how do you stop it, and how would you notice the next attempt?
Key takeaways
- Reproduce first, then choose: a debugger for what you can reproduce, logs for what happens where you cannot look.
- Conditional, exception and field breakpoints stop where the bug is instead of on every pass.
System.Loggerwrites throughjava.util.loggingby default; its DEBUG is FINE and its ERROR is SEVERE.- Levels and handlers are set in
logging.properties, and secrets and personal data never reach a log.
References
- Java SE 25 API: System.Logger (Oracle)
- Java SE 25 API: System.Logger.Level, with the mapping to java.util.logging levels (Oracle)
- Java SE 25 API: System.LoggerFinder (Oracle)
- JEP 264: Platform Logging API and Service (OpenJDK)
- Java Logging Overview (Java SE 25 Core Libraries) (Oracle)
- Java SE 25 API: java.util.logging (Oracle)
- Java SE 25 API: LogManager (java.util.logging.config.file) (Oracle)
- Java SE 25 API: ConsoleHandler (Oracle)
- Java SE 25 API: SimpleFormatter (Oracle)
- The jdb Command (Java SE 25) (Oracle)
- Java Platform Debugger Architecture: Connection and Invocation Details (Oracle)
- Java Debug Wire Protocol (JDWP), Java SE 25: EventRequest.Set modifiers (Oracle)
- Java SE 25 API: com.sun.jdi (Java Debug Interface) (Oracle)
- The jcmd Command (Java SE 25): Thread.print (Oracle)
- OWASP Logging Cheat Sheet (OWASP Foundation)
- CWE-117: Improper Output Neutralization for Logs (MITRE)
- The Digital Personal Data Protection Act, 2023 (Gazette of India), section 8(5) (Ministry of Electronics and Information Technology, Government of India)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress