Tired of hunting for the right log line when a production incident hits?
Messy logs slow you down and hide the real problem.
This post walks through simple, copy-paste Java examples for java.util.logging, Log4j2, and Logback so you can format timestamps, levels, MDC fields, and exceptions the way your ops tools expect.
You’ll see quick implementations for plain text, ANSI-colored terminal output, and JSON structured logs.
By the end you’ll have small, production-ready formatters you can drop into your app.
Core Concepts of Custom Java Log Formatting

A custom log formatter transforms raw log events into formatted text strings ready for output. Your application logs a message, the logging framework creates a LogRecord (java.util.logging) or LogEvent (Log4j2/Logback) object with metadata like timestamp, severity level, logger name, thread identifier, and the message itself. The formatter’s job? Turn that object into something humans can read or machines can parse. Without it, you’d see raw object dumps or framework defaults that never match what you actually need.
Developers build custom formatters to solve real problems. Making logs easier to scan during local development. Generating JSON for ingestion into ELK or Loki. Adding ANSI color codes to highlight errors and warnings in terminals. Including distributed tracing IDs for correlation across microservices. Built-in formatters cover basic use cases, but production environments demand precise timestamp formats, consistent field ordering, or inclusion of Mapped Diagnostic Context (MDC) keys like requestId or traceId. A custom formatter gives you control over every character landing in your log files or stdout.
You’ll write one when your app moves beyond single-server deployments. Containerized workloads need JSON on stdout so orchestrators can ship logs to centralized systems. Distributed systems need correlation IDs in every line. High-traffic services need minimal allocation and fast formatting to avoid GC pressure. If engineers can’t find what they need in logs, or if your observability tools can’t parse the format, a custom formatter is the fix.
Every effective log formatter includes these fields:
Timestamp with millisecond precision in ISO-8601 or a consistent format, always including timezone (prefer UTC).
Log level (INFO, WARN, ERROR) with consistent width or label for filtering and alerting.
Logger name (usually the class or package) to identify where the message came from.
Thread name or ID to correlate logs within concurrent request handling.
MDC/ThreadContext keys like requestId, userId, or traceId for distributed tracing and multi-service correlation.
Exception stack traces formatted for readability or JSON serialization, including cause chains.
Implementing a Custom Formatter Using java.util.logging

Java’s built-in java.util.logging (often called JUL) ships with the JDK and needs no extra dependencies. It provides two built-in Formatter subclasses used by handlers by default, but these rarely meet production needs. Building a custom formatter for JUL is the simplest introduction to Java log formatting because the API surface is small and the contract is clear.
Follow these steps to create a JUL custom formatter:
Create a new class that extends java.util.logging.Formatter.
Override the abstract method public String format(LogRecord record). This method receives a LogRecord object and must return the formatted string that the handler writes to its destination.
Inside format(), call formatMessage(record) to apply any ResourceBundle based message localization and parameter substitution the LogRecord contains.
Build your timestamp using java.time.Instant.ofEpochMilli(record.getMillis()) and a DateTimeFormatter, or use SimpleDateFormat if you’re on older Java versions.
Attach the formatter to a handler programmatically with handler.setFormatter(new MyFormatter()), or configure it in logging.properties by specifying the fully qualified class name.
If you need header or footer text at the start and end of a log file, override getHead(Handler h) and getTail(Handler h).
Here’s a minimal working example that produces timestamp, level, logger, and message on each line:
import java.util.Date;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;
public class MyFormatter extends Formatter {
@Override
public String format(LogRecord record) {
return String.format("%1$tF %1$tT.%1$tL %2$-7s %3$s - %4$s%n",
new Date(record.getMillis()),
record.getLevel(),
record.getLoggerName(),
formatMessage(record));
}
}
You’d wire it up like this: ConsoleHandler ch = new ConsoleHandler(); ch.setFormatter(new MyFormatter()); logger.addHandler(ch);. That’s it. The %1$tF and %1$tT.%1$tL tokens format the date with millisecond precision, %2$-7s left aligns the level in seven characters, and %4$s inserts the message. The %n at the end ensures each log entry ends with a newline so lines don’t merge. You can add thread name with record.getLongThreadID() or exception stack traces by checking record.getThrown() and iterating over the stack trace elements.
Custom Log Formatting with Log4j2 Layouts and Pattern Tokens

Log4j2 is the most flexible and performant logging framework for Java, and its pattern based PatternLayout covers most custom formatting needs without writing code. The pattern string uses placeholders called conversion specifiers that Log4j2 replaces with actual values at runtime. For example, %d{yyyy-MM-dd HH:mm:ss.SSS} outputs the timestamp, %p outputs the log level, %c{1} outputs the logger name (short form with just the last segment), %t outputs the thread name, %m outputs the message, and %n outputs a newline. A production ready pattern might look like this: %d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%t] %c{1} - %m%n.
MDC (Mapped Diagnostic Context) integration is where Log4j2 shines. Call ThreadContext.put("requestId", uuid) at the start of a request, and every log in that thread can include it with %X{requestId} in the pattern. If you want all MDC keys, use %X alone. Exception stack traces are controlled with %ex or %throwable, and you can limit depth with options like %ex{short}. Log4j2 also supports ANSI colorization with %highlight, which automatically colors ERROR in red, WARN in yellow, and INFO in default terminal color. Or you can use %style{%p}{red} for manual control.
| Token | Meaning |
|---|---|
| %d{yyyy-MM-dd HH:mm:ss.SSS} | Timestamp with millisecond precision |
| %p or %-5p | Log level, optionally left-aligned in 5 characters |
| %c{1} | Logger name, shortened to last segment |
| %X{key} | MDC value for the given key |
Creating a Custom Log4j2 Layout
When pattern tokens aren’t enough, extend org.apache.logging.log4j.core.layout.AbstractStringLayout and implement toSerializable(LogEvent event). The LogEvent object exposes everything: event.getInstant() for timestamp, event.getLevel() for severity, event.getLoggerName(), event.getMessage(), event.getContextData() for MDC, and event.getThrown() for exceptions. Build your output string in toSerializable and return it. Annotate the class with @Plugin so Log4j2’s plugin system recognizes it, then reference it in log4j2.xml with <MyCustomLayout/> inside an appender definition. This approach is common when you need custom JSON schemas, binary formats, or performance optimized string building that reuses buffers. Just make sure your Charset handling matches your downstream consumers. UTF-8 is the safe default.
Building Custom Logback Encoders for Advanced Formatting

Logback is the default logging implementation in Spring Boot and many enterprise Java apps. Its pattern syntax is similar to Log4j2 but lives in PatternLayoutEncoder. A typical pattern looks like %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n. The %d token formats timestamps, %thread outputs thread names, %-5level left aligns the log level, %logger{36} truncates logger names to 36 characters, and %msg outputs the message. Like Log4j2, Logback supports MDC with %mdc{key} or %X{key}.
Custom encoders are needed when you want full control over serialization. Emitting binary formats, custom JSON schemas, or adding cryptographic signatures. Extend ch.qos.logback.core.encoder.EncoderBase<ILoggingEvent> and implement three methods: headerBytes() returns bytes written once at the start, encode(ILoggingEvent event) returns the formatted bytes for each log event, and footerBytes() returns bytes written at the end. Inside encode(), extract event.getTimeStamp(), event.getLevel(), event.getLoggerName(), event.getFormattedMessage(), and event.getMDCPropertyMap() to build your output.
To integrate a custom encoder into a Spring Boot application, create src/main/resources/logback-spring.xml and define an appender with your encoder class:
Extend EncoderBase<ILoggingEvent> or LayoutBase<ILoggingEvent> depending on whether you’re producing bytes or strings.
Implement the abstract methods headerBytes(), encode(), and footerBytes() (or doLayout() for layouts).
Reference the encoder in logback-spring.xml with <encoder class="com.example.MyCustomEncoder"/> inside a <appender> block.
Wire the appender to a logger or root logger with <appender-ref ref="CUSTOM"/>.
Spring Boot will automatically load logback-spring.xml and apply your encoder to the configured appenders.
JSON and Structured Logging Formatters in Java

Structured logging means emitting logs as JSON objects instead of plain text, making them machine parseable and ready for ingestion into Elasticsearch, Loki, Datadog, or any observability platform. JSON logs eliminate the need for complex regex parsing and enable rich filtering and aggregation. Modern container orchestrators expect logs on stdout in JSON format so they can collect and ship them without custom log file parsing.
Log4j2 provides JsonLayout out of the box. Configure it in log4j2.xml with <JsonLayout compact="true" eventEol="true"/> inside an appender. This produces one JSON object per line, including timestamp, level, logger, message, MDC fields, and exception details. Logback users should add the logstash-logback-encoder dependency and configure <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"> in logback.xml. This encoder is battle tested in production and supports customization of field names, timestamp formats, and additional context fields.
For java.util.logging, JSON support requires a custom formatter that uses Jackson or Gson to serialize log data. Extend Formatter, build a Map or a POJO from the LogRecord fields, serialize it with your JSON library, and return the resulting string. This approach is more manual but gives you complete control over the schema.
When designing JSON log schemas, always include these fields:
timestamp in ISO-8601 format with timezone (e.g., 2025-05-14T10:32:45.123Z).
level as a string (INFO, WARN, ERROR) for filtering and alerting rules.
logger or source to identify which class or module generated the log.
message containing the human readable log message.
traceId or requestId for distributed tracing and correlation across services.
Adding Colors, ANSI Codes, and Human-Readable Enhancements

Colorized logs make development faster by highlighting errors and warnings in red or yellow, so you spot problems without scanning hundreds of INFO lines. Log4j2 supports color with the %highlight pattern token, which automatically applies ANSI escape codes based on log level. For example, %highlight{%p} outputs ERROR in red and WARN in yellow. Logback and java.util.logging don’t have built-in color support, so you inject ANSI codes manually in your custom formatter.
ANSI escape sequences work on Unix terminals and modern Windows terminals. They’re short strings that change text color or style. To use them, prepend the code to your text and append the reset code. For example, wrapping a log level in red looks like "\u001B[31m" + level + "\u001B[0m". JAnsi is a small library that makes ANSI codes work reliably across platforms, including older Windows versions that don’t support them natively. Call AnsiConsole.systemInstall() at startup, and your codes will work everywhere.
Here are the most useful ANSI codes:
\u001B[31m for Red (use for ERROR)
\u001B[33m for Yellow (use for WARN)
\u001B[32m for Green (use for success messages or DEBUG)
\u001B[0m to Reset to default color
Performance and Production Considerations for Custom Formatters

Logging is on the hot path of every request, so inefficient formatters add latency and trigger garbage collection pauses. The format() or toSerializable() method is called for every log statement, often thousands of times per second in high-throughput services. Avoid heavy operations like regex matching, reflection, or external I/O inside formatters. Prefer StringBuilder over string concatenation to reduce temporary object allocation, and reuse ThreadLocal buffers when safe.
Async appenders are critical for performance. Log4j2’s AsyncAppender and Logback’s AsyncAppender write log events to a bounded queue, then a background thread formats and writes them. This keeps logging off the request thread. Recommended queue sizes are 2048 or 8192 depending on throughput. If the queue fills, the framework can block or drop logs, so monitor queue depth metrics in production. Configure blocking=false if you prefer dropping logs over adding latency.
MDC lookups can be expensive if you store many keys or use complex data structures. Limit MDC to small values like IDs (UUID strings) and avoid nesting or serialization inside MDC values. Always set a Charset explicitly (UTF-8) to avoid platform dependent encoding bugs. Make sure your formatter is thread safe. If you maintain state (like a DateTimeFormatter or StringBuilder), use ThreadLocal or make the formatter stateless.
Follow these six performance tips:
Use StringBuilder or StringBuffer for building formatted strings, not repeated string concatenation.
Avoid calling new Date() or Instant.now() inside the formatter. Use the timestamp provided by the log event (record.getMillis() or event.getInstant()).
Minimize MDC lookups by storing only the keys you need and clearing them after use.
Enable async appenders with bounded queues to decouple formatting from the request thread.
Benchmark your formatter with a standalone test that logs 10,000 messages and measures elapsed time and heap allocation.
Set explicit UTF-8 encoding on all handlers and appenders to avoid charset conversion overhead and bugs.
Testing and Validating Java Log Formatter Output

Custom formatters are easy to break when refactoring, and bugs in log output make production triage harder. Unit tests should verify that your formatter produces exactly the expected string for known inputs. Inject a fixed Clock or TimeProvider so timestamps are deterministic. For java.util.logging, create a LogRecord manually, set its fields, and call format(). For Log4j2, use Log4jLogEvent.newBuilder() to build a test event. Assert the output matches your expected format, character by character.
Integration tests should verify that logs appear in the correct destination (console, file, remote sink) and that rotation, encoding, and newline handling work. Write a log, read the file, and confirm the content. Check that JSON logs are valid JSON (parse them with Jackson or Gson). Verify that MDC keys appear in the output and that exception stack traces include all frames. Test with multi-threaded logging to confirm thread safety.
Run these five checks before deploying a custom formatter:
Fixed timestamp test. Set a known timestamp and verify the output matches the expected format down to milliseconds.
JSON validity. If emitting JSON, parse the output with a JSON library and ensure no syntax errors.
Newline handling. Confirm each log line ends with %n or \n so lines don’t merge.
Charset encoding. Write non-ASCII characters (e.g., emoji or accented letters) and verify they appear correctly in the output file.
Exception formatting. Log a throwable and verify the stack trace is complete and readable.
Common Issues and Troubleshooting Custom Java Formatters

Formatters fail in predictable ways, and the symptoms are easy to spot if you know what to look for. Missing newlines cause log lines to merge into one unreadable string. Wrong charset configuration turns Unicode characters into question marks or mojibake. Heavy logic inside format() blocks the logging thread and adds latency spikes. Misconfigured logger hierarchies let parent loggers override your formatter, so your custom format never runs. Conflicting pattern overrides in property files or XML configs silently replace your formatter.
Here are six issues you’ll hit and how to fix them:
Missing newline at end of format string. Every log line must end with %n (Log4j2/Logback) or \n (JUL). Without it, lines merge. Add the newline token to your pattern or format string.
Wrong charset causes garbled text. Set Charset.forName("UTF-8") explicitly on all handlers, appenders, and encoders. Never rely on platform defaults.
Heavy logic in format() adds latency. Move expensive operations (database lookups, regex, I/O) out of the formatter. Precompute values or use async processing.
Logger hierarchy overrides custom formatter. Check parent loggers in your logging config. If a parent logger sets a different handler or formatter, it will propagate. Set additivity="false" (Logback) or remove parent handlers (JUL).
Pattern config ignored. Verify the config file is being loaded. Print logger.getHandlers() or check LogManager configuration to confirm your custom formatter is attached.
Thread safety violations. If logs from concurrent threads are interleaved or corrupted, your formatter is not thread safe. Use ThreadLocal for stateful objects like DateTimeFormatter.
Final Words
You now have a compact playbook: what formatters do, how to implement them in JUL, Log4j2, and Logback, when JSON makes sense, how to add ANSI color, and how to test and tune for production.
Follow the step‑by‑step sections to build a safe, efficient formatter: prefer StringBuilder, avoid heavy work in format(), test with fixed clocks, and include timestamp, level, logger, thread, MDC, and exceptions.
Pick one framework, copy the sample code, run a quick integration test, and you’ll be shipping clearer logs fast with a custom log formatter java.
FAQ
Q: What is a custom Java log formatter and what does it do?
A: A custom Java log formatter transforms LogRecord or LogEvent objects into text or structured output, letting you control timestamp, level, logger, thread, MDC/context keys, and exception formatting for readability and ingestion.
Q: Why should I create a custom log formatter?
A: Creating a custom formatter improves readability, standardizes fields for observability, enables JSON for aggregation, adds color or context like traceId, and makes logs easier to parse and debug.
Q: When do modern Java apps need custom log formatters?
A: Modern Java apps need custom formatters when default logs lack required fields, when you must send structured JSON to ELK/Loki, need trace/context fields, or want consistent, readable console output.
Q: What key fields should a custom formatter include?
A: Key fields to include are ISO8601 timestamp with milliseconds, level, logger name, thread, MDC/trace/context keys, and full exception details including stack trace for post‑mortem and indexing.
Q: How do I implement a custom formatter using java.util.logging?
A: For java.util.logging implement a Formatter subclass, override format(LogRecord), use formatMessage(), build timestamps from record.getMillis() with java.time, attach via Handler.setFormatter(), and update logging.properties.
Q: How do I include thread and MDC data in Log4j2 patterns?
A: To include thread and MDC in Log4j2 use pattern tokens such as %d{…}, %p, %c{1}, %t for thread and %X{key} for MDC; combine tokens to produce compact, consistent output.
Q: How do I create a custom Log4j2 Layout?
A: To create a custom Log4j2 Layout extend AbstractStringLayout, implement toSerializable(LogEvent), add the @Plugin annotation for registration, and reference the layout in log4j2.xml configuration.
Q: How do I build a custom encoder for Logback?
A: To build a Logback encoder extend EncoderBase or LayoutBase, implement encode()/doEncode, configure the encoder in logback.xml, and override Spring Boot defaults when you need custom output format.
Q: When should I use JSON vs plain text formatters?
A: Use JSON when logs are ingested by ELK/Loki or require structured fields and trace IDs; use plain text for local developer consoles or when human readability matters more than machine parsing.
Q: How can I add safe ANSI colors to logs?
A: Add ANSI colors using Log4j2 %highlight, JAnsi, or manual ANSI codes (31 red, 33 yellow, 32 green, 0 reset), but enable only for terminals and prefer JAnsi for cross‑platform safety.
Q: What are the top performance tips for custom formatters?
A: For performance avoid heavy allocations in format(), use StringBuilder, prefer async appenders with queues (2048–8192), cache lookups, minimize MDC access, and ensure thread‑safe, UTF‑8 output.
Q: How should I test and validate formatter output?
A: Test formatters with a fixed Clock or mocked LogRecord/LogEvent, validate JSON schema and newline handling, assert charset, and run integration tests checking rotation and file size growth.
Q: What are common formatter issues and quick fixes?
A: Common issues include missing newline, wrong charset, heavy format logic causing latency, misconfigured logger hierarchy, conflicting pattern overrides, and handlers not attached—check configs and add unit tests.
