Vibe monitoring with Last9 MCP: Ask your agent to fix production issues! Setup →
Last9 Last9

Apr 8th, ‘25 / 15 min read

Java Util Logging Configuration: A Practical Guide for DevOps & SREs

A hands-on guide to setting up and managing Java Util Logging—built for DevOps and SREs who need clarity, not more config headaches.

Java Util Logging Configuration: A Practical Guide for DevOps & SREs

Setting up proper logging is like having a good navigation system when you're driving through unfamiliar territory.

For DevOps engineers and SREs managing Java applications, understanding how to configure the built-in java.util.logging framework is essential knowledge that can save you hours of troubleshooting headaches.

Let's break down java util logging configuration in a way that makes sense — no fancy jargon, we promise!

What is Java Util Logging?

Java util logging (JUL) is the native logging framework that comes bundled with the Java platform. It's been there since Java 1.4, making it a reliable component in your toolkit. Unlike third-party logging frameworks that require additional dependencies, JUL is ready to use out of the box.

💡
Java Util Logging can be tricky to set up and manage. This guide helps DevOps and SREs get it working right—without all the confusion. Read more

The framework consists of several interconnected components:

Logger Hierarchy

Loggers exist in a hierarchical namespace, typically following your package structure. For example:

  • com.yourcompany (parent logger)
    • com.yourcompany.module1 (child logger)
      • com.yourcompany.module1.submodule (grandchild logger)

Each logger can have:

  • A logging level that controls message filtering
  • Associated handlers that receive log records
  • An optional parent logger (except the root logger)

Log messages propagate up this hierarchy unless specifically configured not to.

Core Components

  • Loggers: Capture messages and route them to handlers
  • Handlers: Publish messages to destinations (console, files, sockets, etc.)
  • Formatters: Determine how messages appear (text format, XML, custom formats)
  • Filters: Provide additional control over which log records to process
  • Levels: Control which messages get recorded based on severity

The complete flow is: Application → Logger → Filter → Handler → Formatter → Output destination

Think of it as your application's black box flight recorder — if set up correctly.

Why Configure Java Util Logging?

You might wonder, "Can't I just use the default settings?" Sure you can — the same way you can wear flip-flops on a hiking trail. It works until it doesn't.

Here's why custom configuration matters:

Default Configuration Limitations

By default, java.util.logging sends:

  • INFORMATION level and higher messages
  • To a ConsoleHandler only
  • Using a SimpleFormatter with minimal information
  • With no log rotation or file output

This works for basic development but falls short in production environments where you need:

  1. Storage Management: Control over what gets logged to conserve disk space
  2. Destination Routing: Ability to send different log levels to different destinations (critical errors to Slack, debug info to files)
  3. Format Customization: Logs formatted to work with log analyzers and monitoring tools
  4. Performance Optimization: Logging configured to minimize impact on application performance
  5. Namespace Control: Different verbosity levels for different parts of your application

For DevOps engineers, proper logging configuration means quicker incident response and easier troubleshooting during those 2 am production alerts. It's the difference between spending minutes versus hours identifying the root cause of an issue.

💡
Java Util Logging can be tricky to set up and manage. This guide helps DevOps and SREs get it working right—without all the confusion. Read more

Understanding the Logging Configuration File

The heart of java util logging configuration is the properties file. By default, it's located at:

$JAVA_HOME/conf/logging.properties

But here's the first tip: don't modify that file. Instead, create your own and point to it using one of these methods:

Method 1: System Property at Launch

java -Djava.util.logging.config.file=/path/to/your/logging.properties YourApp

Method 2: Programmatic Configuration

static {
    try {
        InputStream configFile = MyClass.class.getClassLoader()
            .getResourceAsStream("logging.properties");
        LogManager.getLogManager().readConfiguration(configFile);
    } catch (IOException ex) {
        System.err.println("Could not load configuration file");
        System.err.println(ex.getMessage());
    }
}

Method 3: Using ClassLoader Resources

Place your logging.properties in your application's classpath (e.g., in src/main/resources for Maven projects) and load it:

try {
    final InputStream inputStream = YourClass.class
        .getClassLoader()
        .getResourceAsStream("logging.properties");
    LogManager.getLogManager().readConfiguration(inputStream);
} catch (final IOException e) {
    Logger.getAnonymousLogger().severe("Could not load default logging.properties file");
    Logger.getAnonymousLogger().severe(e.getMessage());
}

This way, your logging configuration travels with your application deployment and can be version-controlled alongside your code.

Essential Properties for Your Logging Configuration

Let's examine the critical settings for your logging.properties file in detail:

Root Logger Configuration

# Root logger level - applies to all loggers without a specific level
.level=INFO

# Root handlers - these receive all logging events not handled by specific logger handlers
handlers=java.util.logging.ConsoleHandler,java.util.logging.FileHandler

# Use these properties to control log propagation up the logger hierarchy
# Setting to false means child loggers won't pass messages to parent handlers
# Default is true
#java.util.logging.Logger.useParentHandlers=true

This establishes the baseline logging level and directs the system to send logs to both console and file handlers. All loggers inherit from this root configuration unless explicitly overridden.

Handler Configuration

# Console handler settings
java.util.logging.ConsoleHandler.level=INFO
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
# Optional encoding property (useful for international characters)
java.util.logging.ConsoleHandler.encoding=UTF-8

# File handler settings
java.util.logging.FileHandler.level=FINE
java.util.logging.FileHandler.pattern=/var/log/myapp/app_%g.log
java.util.logging.FileHandler.limit=50000000
java.util.logging.FileHandler.count=10
java.util.logging.FileHandler.formatter=java.util.logging.XMLFormatter
java.util.logging.FileHandler.append=true
java.util.logging.FileHandler.encoding=UTF-8

Let's break down these FileHandler properties:

  • pattern: Determines the output file location and naming scheme
    • %t = system temp directory
    • %h = user home directory
    • %u = unique number to resolve conflicts
    • %g = generation number for rotated logs
    • %% = escapes the % character
  • limit: Maximum size in bytes before rotating to a new file
  • count: Number of rotating files to maintain
  • append: When true, appends to existing files instead of overwriting
  • encoding: Character encoding for the log files

Notice how we've configured two different handlers with different levels. This is the kind of flexibility you want — less critical info on the console, more detailed logs in the files.

Formatter Configuration

# Format for SimpleFormatter - detailed breakdown of each parameter:
# %1$t... = timestamp (various time/date formats)
# %2$s = source class and method
# %3$s = logger name
# %4$s = log level
# %5$s = message
# %6$s = thrown exception (if any)
# %n = platform-specific line separator
java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %3$s [%2$s] %5$s%6$s%n

This line is your secret weapon. It defines how your log entries will look. The format string uses standard Java formatting syntax where each %n$ refers to the nth parameter:

Parameter Description Example
%1$ Date and time 2023-05-12 14:25:36.789
%2$ Source class and method com.company.MyClass.method
%3$ Logger name com.company.logger
%4$ Log level INFO
%5$ Log message "Database connection established"
%6$ Exception (if present) java.lang.NullPointerException
💡
For a closer look at how system logs actually help during outages and messy on-call shifts, check out this guide.

Custom Handler Example

You can also create and configure custom handlers:

# Custom handler (must be in classpath)
handlers=java.util.logging.ConsoleHandler,java.util.logging.FileHandler,com.yourcompany.logging.SlackNotificationHandler

# Configure the custom handler
com.yourcompany.logging.SlackNotificationHandler.level=SEVERE
com.yourcompany.logging.SlackNotificationHandler.webhookUrl=https://hooks.slack.com/services/your/webhook/url

This shows how you can extend the logging system with your own handlers to integrate with external systems.

Package-Specific Logging Levels

One of the most powerful features of java util logging configuration is the ability to set different logging levels for different packages:

# Set different levels for specific packages
com.yourcompany.authentication.level=FINE
com.yourcompany.authentication.handlers=java.util.logging.FileHandler
com.yourcompany.authentication.useParentHandlers=false

com.yourcompany.database.level=WARNING
com.yourcompany.api.level=INFO

# You can even target specific classes
com.yourcompany.payment.CreditCardProcessor.level=FINEST

This granular control means you can:

  1. Dial up detail in troublesome areas without drowning in logs from well-behaved components
  2. Apply different handlers to different loggers (e.g., sending security logs to a secure storage)
  3. Control log propagation through the logger hierarchy

Logger Hierarchy and Inheritance

Loggers follow Java's package hierarchy. For example:

Root Logger (.)
 └── com.yourcompany
      ├── com.yourcompany.api
      ├── com.yourcompany.service
      │    └── com.yourcompany.service.payment
      └── com.yourcompany.database

By default, loggers inherit:

  • Level settings from their parent (if not explicitly set)
  • All handlers from their parent (controlled by useParentHandlers)

This hierarchy design allows for both broad configuration at the top level and specific overrides where needed.

Preventing Log Propagation

Sometimes you want to isolate logging for certain components. Setting useParentHandlers=false prevents log messages from propagating up the logger hierarchy:

# Security logs should only go to specific handlers, not inherited ones
com.yourcompany.security.level=FINE
com.yourcompany.security.handlers=com.yourcompany.logging.EncryptedFileHandler
com.yourcompany.security.useParentHandlers=false

This ensures sensitive logs only go to their designated handlers, not to other configured handlers higher in the hierarchy.

💡
​Understanding log levels is crucial for effective system monitoring and troubleshooting. For a comprehensive overview of different log levels and their appropriate usage, check out this guide.​

Common Logging Levels Explained

Java util logging provides seven standard logging levels, plus two special levels (OFF and ALL). Let's examine each in detail:

Level Integer Value Purpose Typical Usage Example Message
SEVERE 1000 Critical issues Application crashes, data corruption, security breaches "Database connection failed - application cannot continue"
WARNING 900 Potential problems Issues that don't halt execution but need attention "API rate limit at 90% - throttling may occur soon"
INFO 800 Normal operation Startup/shutdown events, major state changes "Application started successfully in 4.2 seconds"
CONFIG 700 Configuration details System settings, environment information "Using database configuration: host=db1.example.com, maxConnections=50"
FINE 500 Basic tracing Entry/exit points of significant methods "Entering method processPayment() with orderId=12345"
FINER 400 Detailed tracing Algorithm steps, loop iterations "Authentication step 2: validating token signature"
FINEST 300 Highly detailed Low-level operations, variable values "Loop iteration 7: currentValue=42, accumulator=295"
OFF Integer.MAX_VALUE Disable logging When you need to silence a noisy component N/A
ALL Integer.MIN_VALUE Enable all logs During detailed debugging sessions N/A

Custom Levels

You can also create custom logging levels for specific needs:

// Define a custom level
static final Level AUDIT = new CustomLevel("AUDIT", 950);

// Custom level implementation
static class CustomLevel extends Level {
    protected CustomLevel(String name, int value) {
        super(name, value);
    }
}

Performance Considerations

Each logging level has performance implications:

  1. Disabled logging statements (below the configured level) have minimal impact - just a simple integer comparison
  2. Enabled logging requires string formatting, object serialization, I/O operations

For high-throughput applications, consider:

// Efficient logging pattern - avoid string concatenation when level is disabled
if (logger.isLoggable(Level.FINE)) {
    logger.fine("Complex operation result: " + expensiveToString(result));
}

Pro tip: In production, stick with INFO and above unless you're troubleshooting something specific. Your storage budget will thank you.

Logging Configuration Best Practices for DevOps

As a DevOps engineer or SRE, these advanced practices will save you from common operational headaches:

Implement Robust File Rotation

# Basic file rotation
java.util.logging.FileHandler.pattern=/var/log/myapp/app_%g.log
java.util.logging.FileHandler.limit=50000000
java.util.logging.FileHandler.count=10

# For pattern options:
# %h = user home directory
# %t = system temp directory
# %u = unique number to resolve conflicts
# %g = generation number for rotation
# %% = percent sign

# Add timestamp to filenames for easier log file management
java.util.logging.FileHandler.pattern=/var/log/myapp/app_%g_%u_%t.log

This creates a rotation of 10 log files, each up to 50MB in size. When they fill up, the oldest gets replaced. No more "oops, we ran out of disk space" incidents.

Implement Environment-Specific Configurations

Don't use the same logging configuration across all environments. Create separate files:

development-logging.properties:

.level=FINE
handlers=java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level=FINE

testing-logging.properties:

.level=CONFIG
handlers=java.util.logging.ConsoleHandler,java.util.logging.FileHandler
java.util.logging.FileHandler.pattern=/var/log/myapp/test_%g.log

production-logging.properties:

.level=INFO
handlers=java.util.logging.FileHandler
java.util.logging.FileHandler.pattern=/var/log/myapp/prod_%g.log

Load the appropriate file based on an environment variable:

java -Djava.util.logging.config.file=${ENV_CONFIG_DIR}/${ENV_NAME}-logging.properties MyApp

Add Contextual Information with MDC-like Pattern

While JUL doesn't have built-in MDC (Mapped Diagnostic Context) like Log4j, you can simulate it:

// Create a custom formatter that adds thread-local context
public class ContextAwareFormatter extends SimpleFormatter {
    private static final ThreadLocal<Map<String, String>> contextMap = 
        ThreadLocal.withInitial(HashMap::new);
    
    public static void putContext(String key, String value) {
        contextMap.get().put(key, value);
    }
    
    public static void clearContext() {
        contextMap.get().clear();
    }
    
    @Override
    public String format(LogRecord record) {
        StringBuilder sb = new StringBuilder(super.format(record));
        // Insert context before newline
        int insertPos = sb.length() - 1;
        Map<String, String> context = contextMap.get();
        if (!context.isEmpty()) {
            sb.insert(insertPos, " Context{" + context + "}");
        }
        return sb.toString();
    }
}

// Usage
ContextAwareFormatter.putContext("requestId", "abc-123");
ContextAwareFormatter.putContext("userId", "user-456");
logger.info("Processing request");  // Will include the context

Include Essential Context in Log Formats

Make sure your log format includes these critical components:

# Complete logging format with extended context
java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s [thread:%9$s] [%3$s] %2$s: %5$s%6$s%n

Elements to include:

  • Timestamp with milliseconds: Essential for correlating events across services
  • Log level: For quick visual filtering
  • Thread name/ID: Critical for diagnosing concurrency issues
  • Logger name: Identifies the source component
  • Source class/method: Pinpoints the exact location in code
  • Message: The actual log content
  • Exception details: Full stack traces when exceptions occur
💡
Running into java.lang.OutOfMemoryError can be a real headache. This guide breaks down why it happens and how to fix it without making things worse.

Configure for Container Environments

In containerized deployments:

# Direct logs to stdout/stderr for container log collection
handlers=java.util.logging.ConsoleHandler

# Don't create local files that would be lost when containers are replaced
# java.util.logging.FileHandler.pattern=/var/log/app.log

# Use a format compatible with log aggregation systems
java.util.logging.SimpleFormatter.format={"timestamp": "%1$tFT%1$tT.%1$tLZ", "level": "%4$s", "thread": "%9$s", "logger": "%3$s", "message": "%5$s", "exception": "%6$s"}%n

This JSON format works well with systems like ELK Stack, Loki, or CloudWatch.

Dynamic Reconfiguration of Logging

One of the less-known features of java util logging is the ability to change logging levels without restarting your application. This can be a lifesaver when troubleshooting production issues.

Reconfiguring the Entire Logging System

You can reload the entire configuration at runtime:

import java.util.logging.*;
import java.io.*;

// Load an updated configuration file
try {
    LogManager.getLogManager().readConfiguration(
        new FileInputStream("/path/to/updated/logging.properties"));
} catch (IOException e) {
    e.printStackTrace();
}

Modifying Specific Logger Levels

For more surgical changes, you can target specific loggers:

// Get a specific logger and change its level
Logger logger = Logger.getLogger("com.yourcompany.problematic.component");
logger.setLevel(Level.FINEST);

// Return to normal after gathering information
// (can be triggered by a scheduled task)
Thread.sleep(60000); // Collect detailed logs for 1 minute
logger.setLevel(Level.INFO);

JMX-Based Remote Configuration

For production systems, implement JMX controls to allow remote logging changes:

import javax.management.*;
import java.lang.management.*;

// Create a MBean for log level management
public class LoggingController implements LoggingControllerMBean {
    @Override
    public void setLogLevel(String loggerName, String levelName) {
        Logger logger = Logger.getLogger(loggerName);
        Level level = Level.parse(levelName);
        logger.setLevel(level);
    }
    
    @Override
    public String getLogLevel(String loggerName) {
        Logger logger = Logger.getLogger(loggerName);
        Level level = logger.getLevel();
        return level != null ? level.getName() : "inherited";
    }
}

// MBean interface
public interface LoggingControllerMBean {
    void setLogLevel(String loggerName, String levelName);
    String getLogLevel(String loggerName);
}

// Register with JMX
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("com.yourcompany:type=LoggingController");
LoggingController controller = new LoggingController();
mbs.registerMBean(controller, name);

This approach creates a management interface that allows operators to:

  1. Adjust logging levels remotely using JMX tools like JConsole or VisualVM
  2. Implement automated logging adjustments based on system metrics
  3. Create a web-based admin interface for logging management

REST API-Based Configuration

For microservices, implement a dedicated endpoint:

@RestController
@RequestMapping("/admin/logging")
public class LoggingController {
    @PutMapping("/{loggerName}")
    public ResponseEntity<String> setLogLevel(
            @PathVariable String loggerName,
            @RequestParam String level,
            @RequestHeader("X-Admin-Token") String adminToken) {
        
        // Validate admin token first
        if (!validateAdminToken(adminToken)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        
        try {
            Logger logger = Logger.getLogger(loggerName);
            Level newLevel = Level.parse(level.toUpperCase());
            Logger.getLogger(loggerName).setLevel(newLevel);
            return ResponseEntity.ok("Logger " + loggerName + " set to " + level);
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body("Invalid log level: " + level);
        }
    }
}

This approach allows you to adjust logging verbosity on the fly, get the information you need, and then turn it back down—all without disrupting your service or deploying new configurations.

Probo Cuts Monitoring Costs by 90% with Last9
Probo Cuts Monitoring Costs by 90% with Last9

Integrating with Modern Logging Ecosystems

While java util logging is built into the JDK, modern DevOps practices require integration with centralized logging systems.

Here's how to connect JUL with popular observability platforms:

Connecting Java Util Logging (JUL) with Last9

1. Bridge JUL to SLF4J
JUL doesn’t directly integrate with observability tools. To start, route JUL logs through SLF4J:

  • Add these dependencies to your project:
    • jul-to-slf4j
    • slf4j-api

In your application startup code:

java.util.logging.LogManager.getLogManager().reset();
org.slf4j.bridge.SLF4JBridgeHandler.install();

2. Configure a Logging Backend (Logback or Log4j2)
Once logs are routed through SLF4J, use a logging backend to handle them:

  • Recommended: Last9 or Log4j2
  • Configure the backend to emit logs in a structured format (JSON recommended).

3. Set Up Last9 as the Logging Backend via OTLP
Last9 supports ingesting logs using the OpenTelemetry Protocol (OTLP):

  • Use OpenTelemetry SDK or a collector to export logs in OTLP format (gRPC or HTTP).
  • Point the exporter to your Last9 OTLP endpoint.
  • Ensure logs include relevant context like service name, environment, and trace ID (if available).

4. View Logs in Context Inside Last9
Once your logs are flowing into Last9:

  • They appear alongside metrics and traces—correlated automatically.
  • You get a single-pane view of system behavior, great for debugging and alert triage.
  • No need for separate logging infrastructure like ELK, Loki, or a sidecar setup.

With this setup, JUL logs become first-class citizens in your observability stack—fully integrated into Last9’s telemetry platform.

💡
For more on how to integrate and send logs to Last9, check out our logs documentation.

Integration with ELK Stack (Elasticsearch, Logstash, Kibana)

Create a custom handler that sends logs directly to Logstash:

public class LogstashHandler extends Handler {
    // Implementation details that format and send log records 
    // to Logstash via HTTP in JSON format
}

The key points for ELK integration:

  • Format logs as structured JSON that Elasticsearch can easily index
  • Include metadata fields like application name, host, environment
  • Use bulk API endpoints for better performance
  • Consider using a buffer to reduce network overhead

Integration with AWS CloudWatch

For AWS environments, you can send logs directly to CloudWatch:

public class CloudWatchLogsHandler extends Handler {
    // Implementation for batching and sending logs to CloudWatch
}

Important considerations:

  • Use the AWS SDK for Java to interact with CloudWatch Logs API
  • Batch logs to reduce API calls and costs
  • Include AWS resource identifiers (EC2 instance ID, ECS task, etc.)
  • Set up proper IAM permissions for your service

Using SLF4J as a Bridge

Rather than creating custom handlers, you can use SLF4J as a facade:

  1. Add these dependencies to your project:
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.36</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>1.7.36</version>
</dependency>
  1. Replace direct JUL usage with SLF4J:
// Instead of:
// private static final Logger logger = Logger.getLogger(MyClass.class.getName());

// Use:
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MyClass.class);

This approach gives you the flexibility to switch logging implementations without changing the application code.

Structured Logging with JSON

Modern log aggregation systems work best with structured data. Create a JSON formatter:

public class JsonFormatter extends Formatter {
    private final ObjectMapper mapper = new ObjectMapper();
    
    @Override
    public String format(LogRecord record) {
        Map<String, Object> logData = new HashMap<>();
        // Populate log data fields from the record
        return mapper.writeValueAsString(logData);
    }
}

Benefits of structured logging:

  • Easier to search and filter in aggregation systems
  • Enables automated alerts based on specific field values
  • Supports visualization and dashboard creation
  • Facilitates log analysis across multiple services

The goal is to have your Java logs play nicely with your broader observability strategy while maintaining the simplicity of using the JDK's built-in logging framework.

Troubleshooting Common Logging Issues

When java util logging isn't behaving as expected, check these common culprits:

No Logs Appearing

If logs are mysteriously absent:

    • Verify your config file is being loaded properly
    • Check file paths and permissions
    • Add a deliberate syntax error to see if the file is being processed (you'll see an exception)
    • Logger levels might be set higher than your log statements
    • Remember that logger levels are hierarchical
    • Handlers might not be properly attached to loggers
    • File handlers might lack write permissions

Handler Configuration IssuesDiagnostic approach:

// Check if handlers are attached
Logger root = Logger.getLogger("");
System.out.println("Root handlers count: " + root.getHandlers().length);

Level Threshold Too HighDiagnostic approach:

// Add this to confirm logger levels
Logger logger = Logger.getLogger("com.yourcompany");
System.out.println("Logger level: " + logger.getLevel());
System.out.println("Logger parent: " + logger.getParent().getName());
System.out.println("Parent level: " + logger.getParent().getLevel());

Configuration Loading IssuesDiagnostic approach:

// Add this to your startup code to see where Java is looking for the config
System.out.println("Logging config file location: " + 
    System.getProperty("java.util.logging.config.file"));
💡
Catch and fix Java Util Logging issues before they hit prod—right from your IDE, using AI and Last9 MCP.

Too Many Logs

If you're drowning in logs:

    • Check for overly verbose loggers, especially in third-party libraries
    • Use a targeted approach with more specific logger names
    • Create custom filters to remove repetitive messages
    • Log only a percentage of high-volume events

Use Sampling for High-Volume ComponentsSampling example:

// Log only 1% of these messages
if (Math.random() < 0.01) {
    logger.fine("High volume operation completed");
}

Apply Filters to Exclude NoiseSimple filter example:

public class RepetitiveMessageFilter implements Filter {
    private String lastMessage = "";
    private int repeatCount = 0;
    
    @Override
    public boolean isLoggable(LogRecord record) {
        // Filter implementation that drops repetitive messages
    }
}

Review Level SettingsAdjustment technique:

# Set chatty libraries to a higher threshold
org.hibernate.level=WARNING
org.apache.http.level=WARNING

Wrong Format or Missing Information

If the log format isn't what you expect:

  1. Double-check Formatter Configuration
    • Ensure your format string is correctly defined
    • Verify formatter is assigned to the handlers
    • Try simplified format strings to isolate issues
  2. Check for Character Encoding Issues
    • Ensure consistent encoding between writing and reading logs
    • Set explicit encoding in handler configuration

Test Format String SeparatelyTesting approach:

// Test formatter directly
Formatter formatter = new SimpleFormatter();
LogRecord record = new LogRecord(Level.INFO, "Test message");
record.setSourceClassName("TestClass");
record.setSourceMethodName("testMethod");
System.out.println(formatter.format(record));

Testing Logging Configuration

A systematic approach to validate your logging setup:

  1. Create a simple test class that logs at various levels
  2. Run it with your configuration
  3. Verify logs appear as expected in all destinations
  4. Check log rotation by forcing rotation events

Example test class:

public class LoggingTest {
    private static final Logger logger = Logger.getLogger(LoggingTest.class.getName());
    
    public static void main(String[] args) {
        // Log at all levels to test configuration
        logger.severe("Severe message test");
        logger.warning("Warning message test");
        logger.info("Info message test");
        logger.config("Config message test");
        logger.fine("Fine message test");
        logger.finer("Finer message test");
        logger.finest("Finest message test");
        
        // Test exception logging
        try {
            throw new Exception("Test exception");
        } catch (Exception e) {
            logger.log(Level.SEVERE, "Exception test", e);
        }
    }
}

Wrap Up

Mastering java util logging configuration is about finding that sweet spot between too much information and not enough.

The built-in logging framework might not be the newest or flashiest tool in the Java ecosystem, but it's reliable, always available, and more powerful than many developers realize.

Well-configured logging means:

  1. Faster incident resolution - When issues arise, your logs provide clear breadcrumbs to follow
  2. Lower storage costs - By logging only what matters, you avoid wasting resources
  3. Better system insights - Strategic logging reveals performance bottlenecks and usage patterns
  4. Reduced technical debt - A good logging strategy scales with your application and evolves with your needs
💡
Do you have questions or some logging tricks of your own? Join our Discord Community where we chat about DevOps, Java, and the occasional debugging war story.

Contents


Newsletter

Stay updated on the latest from Last9.

Authors
Anjali Udasi

Anjali Udasi

Helping to make the tech a little less intimidating. I love breaking down complex concepts into easy-to-understand terms.