pl.morgwai.base.logging.JulFormatter Maven / Gradle / Ivy
// Copyright (c) Piotr Morgwai Kotarbinski, Licensed under the Apache License, Version 2.0
package pl.morgwai.base.logging;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.logging.*;
/**
* A text log formatter similar to {@link java.util.logging.SimpleFormatter} that additionally
* allows to format stack trace elements and to add
* {@link LogRecord#getSequenceNumber() log sequence id} and
* {@link LogRecord#getThreadID() thread id} to log entries.
*/
public class JulFormatter extends Formatter {
/**
* Name of the logging or system property containing the main format for each record.
* @see #format(LogRecord)
*/
public static final String FORMAT_PROPERTY = JulFormatter.class.getName() + ".format";
final String format;
/**
* Name of the logging or system property containing the format for stack frames of logged
* {@link Throwable}s.
* @see #format(LogRecord)
*/
public static final String STACKFRAME_FORMAT_PROPERTY =
JulFormatter.class.getName() + ".stackFrameFormat";
final String stackFrameFormat;
/**
* Creates a new formatter configured using supplied params.
* @param format the main format for log records.
* If it's {@code null} then {@value #DEFAULT_FORMAT} is used.
* @param stackFrameFormat format for stack trace elements of logged {@link Throwable}s.
* @see #format(LogRecord)
*/
public JulFormatter (String format, String stackFrameFormat) {
if (format != null) {
this.format = format;
} else {
this.format = DEFAULT_FORMAT;
}
this.stackFrameFormat = stackFrameFormat;
}
/**
* {@value #DEFAULT_FORMAT}.
* "{sequenceId} {threadId} {level} {timestamp} {loggerName} {message} {thrown}".
*/
public static final String DEFAULT_FORMAT =
"%7$5d %8$3d %4$7s %1$tF %1$tT.%1$tL %3$s %5$s %6$s%n";
/**
* Creates a new formatter configured using either system properties or logging properties.
* If both are present, system properties take precedence.
*
* By default the value of {@link #FORMAT_PROPERTY} property is used as the main format
* for log records. If it is not present in either logging or system properties, then
* {@value #JUL_SIMPLE_FORMAT_PROPERTY_NAME} property is read and if present, its value is
* prepended with {@code "%7$5d %8$3d "} and used instead. if it is also absent, then
* {@value #DEFAULT_FORMAT} is used.
*
* The value of {@link #STACKFRAME_FORMAT_PROPERTY} property is used as the format for
* stack trace elements. If it is not present in either logging or system properties, then
* {@code null} is passed.
*
* @see #JulFormatter(String, String)
* @see #format(LogRecord)
*/
public JulFormatter() {
this(getFormatFromProperties(), getStackFrameFormatFromProperties());
}
static String getFormatFromProperties() {
var format = System.getProperty(FORMAT_PROPERTY);
if (format == null) format = LogManager.getLogManager().getProperty(FORMAT_PROPERTY);
if (format == null) {
var simpleFormat = System.getProperty(JUL_SIMPLE_FORMAT_PROPERTY_NAME);
if (simpleFormat == null) {
simpleFormat = LogManager.getLogManager().getProperty(
JUL_SIMPLE_FORMAT_PROPERTY_NAME);
}
if (simpleFormat != null) {
format = "%7$5d %8$3d " + simpleFormat;
}
}
return format;
}
/**
* {@value #JUL_SIMPLE_FORMAT_PROPERTY_NAME}
*/
public static final String JUL_SIMPLE_FORMAT_PROPERTY_NAME =
"java.util.logging.SimpleFormatter.format";
static String getStackFrameFormatFromProperties() {
final var stackFrameFormat = System.getProperty(STACKFRAME_FORMAT_PROPERTY);
if (stackFrameFormat != null) return stackFrameFormat;
return LogManager.getLogManager().getProperty(STACKFRAME_FORMAT_PROPERTY);
}
/**
* Formats the given {@code record}.
*
* The result is obtained by running
* {@link String#format(String, Object...)
* String.format(format, timestamp, source, loggerName, level, message, formattedThrown, logId,
* threadId)}
* where {@code format} is obtained from either {@link #FORMAT_PROPERTY} property or the
* first param of {@link #JulFormatter(String, String)}.
*
* {@code formattedThrown} is obtained
* by calling {@code record.getThrown().toString()} and appending
* {@link String#format(String, Object...) String.format(stackFrameFormat, logId, className,
* methodName, FileName, lineNumber, moduleName, moduleVersion, classLoaderName)}
* where {@code stackFrameFormat} is obtained from either
* {@link #STACKFRAME_FORMAT_PROPERTY} property or the second param of
* {@link #JulFormatter(String, String)}.
* If {@code stackFrameFormat} is {@code null} then
* {@link Throwable#printStackTrace(java.io.PrintStream)} is called instead of
* {@link String#format(String, Object...)}.
*/
@Override
public String format(LogRecord record) {
final var timestamp = ZonedDateTime.ofInstant(record.getInstant(), ZoneId.systemDefault());
String source;
if (record.getSourceClassName() != null) {
source = record.getSourceClassName();
if (record.getSourceMethodName() != null) {
source += '.' + record.getSourceMethodName();
}
} else {
source = record.getLoggerName();
}
return String.format(
format,
timestamp,
source,
record.getLoggerName(),
record.getLevel().getLocalizedName(),
formatMessage(record),
getFormattedThrown(record),
record.getSequenceNumber(),
record.getThreadID());
}
String getFormattedThrown(LogRecord record) {
final var thrown = record.getThrown();
if (thrown == null) return "";
if (stackFrameFormat == null) {
try (
var sw = new StringWriter();
var pw = new PrintWriter(sw);
) {
pw.println();
thrown.printStackTrace(pw);
return sw.toString();
} catch (IOException ignored) {} // StringWriter.close() is no-op
}
final var throwableStringBuilder = new StringBuilder(thrown.toString());
for (var stackFrame: thrown.getStackTrace()) {
throwableStringBuilder.append(String.format(
stackFrameFormat,
record.getSequenceNumber(),
record.getThreadID(),
stackFrame.getClassName(),
stackFrame.getMethodName(),
stackFrame.getFileName(),
stackFrame.getLineNumber(),
stackFrame.getModuleName(),
stackFrame.getModuleVersion(),
stackFrame.getClassLoaderName()));
}
return throwableStringBuilder.toString();
}
}