org.jboss.logmanager.formatters.StructuredFormatter Maven / Gradle / Ivy
Show all versions of jboss-logmanager-embedded Show documentation
/*
* Copyright 2018 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.logmanager.formatters;
import java.io.PrintWriter;
import java.io.Writer;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.IdentityHashMap;
import java.util.Map;
import org.jboss.logmanager.ExtFormatter;
import org.jboss.logmanager.ExtLogRecord;
import org.jboss.logmanager.PropertyValues;
/**
* An abstract class that uses a generator to help generate structured data from a {@link
* org.jboss.logmanager.ExtLogRecord record}.
*
* Note that including details can be expensive in terms of calculating the caller.
*
*
* By default the {@linkplain #setRecordDelimiter(String) record delimiter} is set to {@code \n}.
*
*
* @author James R. Perkins
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public abstract class StructuredFormatter extends ExtFormatter {
/**
* The key used for the structured log record data.
*/
public enum Key {
EXCEPTION("exception"),
EXCEPTION_CAUSED_BY("causedBy"),
EXCEPTION_CIRCULAR_REFERENCE("circularReference"),
EXCEPTION_TYPE("exceptionType"),
EXCEPTION_FRAME("frame"),
EXCEPTION_FRAME_CLASS("class"),
EXCEPTION_FRAME_LINE("line"),
EXCEPTION_FRAME_METHOD("method"),
EXCEPTION_FRAMES("frames"),
EXCEPTION_MESSAGE("message"),
EXCEPTION_REFERENCE_ID("refId"),
EXCEPTION_SUPPRESSED("suppressed"),
HOST_NAME("hostName"),
LEVEL("level"),
LOGGER_CLASS_NAME("loggerClassName"),
LOGGER_NAME("loggerName"),
MDC("mdc"),
MESSAGE("message"),
NDC("ndc"),
PROCESS_ID("processId"),
PROCESS_NAME("processName"),
RECORD("record"),
SEQUENCE("sequence"),
SOURCE_CLASS_NAME("sourceClassName"),
SOURCE_FILE_NAME("sourceFileName"),
SOURCE_LINE_NUMBER("sourceLineNumber"),
SOURCE_METHOD_NAME("sourceMethodName"),
SOURCE_MODULE_NAME("sourceModuleName"),
SOURCE_MODULE_VERSION("sourceModuleVersion"),
STACK_TRACE("stackTrace"),
THREAD_ID("threadId"),
THREAD_NAME("threadName"),
TIMESTAMP("timestamp");
private final String key;
Key(final String key) {
this.key = key;
}
/**
* Returns the name of the key for the structure.
*
* @return the name of they key
*/
public String getKey() {
return key;
}
}
/**
* Defines the way a cause will be formatted.
*/
public enum ExceptionOutputType {
/**
* The cause, if present, will be an array of stack trace elements. This will include suppressed exceptions and
* the {@linkplain Throwable#getCause() cause} of the exception.
*/
DETAILED,
/**
* The cause, if present, will be a string representation of the stack trace in a {@code stackTrace} property.
* The property value is a string created by {@link Throwable#printStackTrace()}.
*/
FORMATTED,
/**
* Adds both the {@link #DETAILED} and {@link #FORMATTED}
*/
DETAILED_AND_FORMATTED
}
private final Map keyOverrides;
private final String keyOverridesValue;
private volatile boolean printDetails;
private volatile String eorDelimiter = "\n";
// Guarded by this
private DateTimeFormatter dateTimeFormatter;
// Guarded by this
private ZoneId zoneId;
private volatile ExceptionOutputType exceptionOutputType;
private final StringBuilderWriter writer = new StringBuilderWriter();
// Guarded by this
private int refId;
protected StructuredFormatter() {
this(null, null);
}
protected StructuredFormatter(final Map keyOverrides) {
this(keyOverrides, PropertyValues.mapToString(keyOverrides));
}
protected StructuredFormatter(final String keyOverrides) {
this(PropertyValues.stringToEnumMap(Key.class, keyOverrides), keyOverrides);
}
private StructuredFormatter(final Map keyOverrides, final String keyOverridesValue) {
this.keyOverridesValue = keyOverridesValue;
this.printDetails = false;
zoneId = ZoneId.systemDefault();
dateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(zoneId);
this.keyOverrides = (keyOverrides == null ? Collections.emptyMap() : new EnumMap<>(keyOverrides));
exceptionOutputType = ExceptionOutputType.DETAILED;
}
/**
* Creates the generator used to create the structured data.
*
* @return the generator to use
*
* @throws Exception if an error occurs creating the generator
*/
protected abstract Generator createGenerator(Writer writer) throws Exception;
/**
* Invoked before the structured data is added to the generator.
*
* @param generator the generator to use
* @param record the log record
*/
protected void before(final Generator generator, final ExtLogRecord record) throws Exception {
// do nothing
}
/**
* Invoked after the structured data has been added to the generator.
*
* @param generator the generator to use
* @param record the log record
*/
protected void after(final Generator generator, final ExtLogRecord record) throws Exception {
// do nothing
}
/**
* Checks to see if the key should be overridden.
*
* @param defaultKey the default key
*
* @return the overridden key or the default key if no override exists
*/
protected final String getKey(final Key defaultKey) {
if (keyOverrides.containsKey(defaultKey)) {
return keyOverrides.get(defaultKey);
}
return defaultKey.getKey();
}
@Override
public final synchronized String format(final ExtLogRecord record) {
final boolean details = printDetails;
try {
final Generator generator = createGenerator(writer).begin();
before(generator, record);
// Add the default structure
generator.add(getKey(Key.TIMESTAMP), dateTimeFormatter.format(Instant.ofEpochMilli(record.getMillis())))
.add(getKey(Key.SEQUENCE), record.getSequenceNumber())
.add(getKey(Key.LOGGER_CLASS_NAME), record.getLoggerClassName())
.add(getKey(Key.LOGGER_NAME), record.getLoggerName())
.add(getKey(Key.LEVEL), record.getLevel().getName())
.add(getKey(Key.MESSAGE), formatMessage(record))
.add(getKey(Key.THREAD_NAME), record.getThreadName())
.add(getKey(Key.THREAD_ID), record.getThreadID())
.add(getKey(Key.MDC), record.getMdcCopy())
.add(getKey(Key.NDC), record.getNdc());
if (isNotNullOrEmpty(record.getHostName())) {
generator.add(getKey(Key.HOST_NAME), record.getHostName());
}
if (isNotNullOrEmpty(record.getProcessName())) {
generator.add(getKey(Key.PROCESS_NAME), record.getProcessName());
}
final long processId = record.getProcessId();
if (processId >= 0) {
generator.add(getKey(Key.PROCESS_ID), record.getProcessId());
}
// Add the cause of the log message if applicable
final Throwable thrown = record.getThrown();
if (thrown != null) {
if (isDetailedExceptionOutputType()) {
refId = 0;
final Map seen = new IdentityHashMap<>();
generator.startObject(getKey(Key.EXCEPTION));
addException(generator, thrown, seen);
generator.endObject();
}
if (isFormattedExceptionOutputType()) {
final StringBuilderWriter w = new StringBuilderWriter();
thrown.printStackTrace(new PrintWriter(w));
generator.add(getKey(Key.STACK_TRACE), w.toString());
}
}
if (details) {
generator.add(getKey(Key.SOURCE_CLASS_NAME), record.getSourceClassName())
.add(getKey(Key.SOURCE_FILE_NAME), record.getSourceFileName())
.add(getKey(Key.SOURCE_METHOD_NAME), record.getSourceMethodName())
.add(getKey(Key.SOURCE_LINE_NUMBER), record.getSourceLineNumber())
.add(getKey(Key.SOURCE_MODULE_NAME), record.getSourceModuleName())
.add(getKey(Key.SOURCE_MODULE_VERSION), record.getSourceModuleVersion());
}
after(generator, record);
generator.end();
// Append an EOL character if desired
if (getRecordDelimiter() != null) {
writer.append(getRecordDelimiter());
}
return writer.toString();
} catch (Exception e) {
// Wrap and rethrow
throw new RuntimeException(e);
} finally {
// Clear the writer for the next format
writer.clear();
}
}
@Override
public boolean isCallerCalculationRequired() {
return isPrintDetails();
}
/**
* A string representation of the key overrides. The default is {@code null}.
*
* @return a string representation of the key overrides or {@code null} if no overrides were configured
*/
public String getKeyOverrides() {
return keyOverridesValue;
}
/**
* Returns the character used to indicate the record has is complete. This defaults to {@code \n} and may be
* {@code null} if no end of record character is desired.
*
* @return the end of record delimiter or {@code null} if no delimiter is desired
*/
public String getRecordDelimiter() {
return eorDelimiter;
}
/**
* Sets the value to be used to indicate the end of a record. If set to {@code null} no delimiter will be used at
* the end of the record.
*
* @param eorDelimiter the delimiter to be used or {@code null} to not use a delimiter
*/
public void setRecordDelimiter(final String eorDelimiter) {
this.eorDelimiter = eorDelimiter;
}
/**
* Returns the current formatter used to format a records date and time.
*
* @return the current formatter
*/
public synchronized DateTimeFormatter getDateTimeFormatter() {
return dateTimeFormatter;
}
/**
* Sets the pattern to use when formatting the date. The pattern must be a valid
* {@link java.time.format.DateTimeFormatter#ofPattern(String)} pattern.
*
* If the pattern is {@code null} a default {@linkplain DateTimeFormatter#ISO_OFFSET_DATE_TIME formatter} will be
* used. The {@linkplain #setZoneId(String) zone id} will always be appended to the formatter. By default the zone
* id will default to the {@linkplain ZoneId#systemDefault() systems zone id}.
*
*
* @param pattern the pattern to use or {@code null} to use a default pattern
*/
public synchronized void setDateFormat(final String pattern) {
if (pattern == null) {
dateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(zoneId);
} else {
dateTimeFormatter = DateTimeFormatter.ofPattern(pattern).withZone(zoneId);
}
}
/**
* Returns the current zone id used for the {@linkplain #getDateTimeFormatter() date and time formatter}.
*
* @return the current zone id
*/
public synchronized ZoneId getZoneId() {
return zoneId;
}
/**
* Sets the {@link ZoneId} to use when formatting the date and time from the {@link java.util.logging.LogRecord}.
*
* The rules of the id must conform to the rules specified on {@link ZoneId#of(String)}.
*
*
* @param zoneId the zone id or {@code null} to use the {@linkplain ZoneId#systemDefault() system default}
*
* @see ZoneId#of(String)
*/
public void setZoneId(final String zoneId) {
final ZoneId changed;
if (zoneId == null) {
changed = ZoneId.systemDefault();
} else {
changed = ZoneId.of(zoneId);
}
synchronized (this) {
this.zoneId = changed;
dateTimeFormatter = dateTimeFormatter.withZone(changed);
}
}
/**
* Indicates whether or not details should be printed.
*
* @return {@code true} if details should be printed, otherwise {@code false}
*/
public boolean isPrintDetails() {
return printDetails;
}
/**
* Sets whether or not details should be printed.
*
* Printing the details can be expensive as the values are retrieved from the caller. The details include the
* source class name, source file name, source method name and source line number.
*
*
* @param printDetails {@code true} if details should be printed
*/
public void setPrintDetails(@SuppressWarnings("SameParameterValue") final boolean printDetails) {
this.printDetails = printDetails;
}
/**
* Get the current output type for exceptions.
*
* @return the output type for exceptions
*/
public ExceptionOutputType getExceptionOutputType() {
return exceptionOutputType;
}
/**
* Set the output type for exceptions. The default is {@link ExceptionOutputType#DETAILED DETAILED}.
*
* @param exceptionOutputType the desired output type, if {@code null} {@link ExceptionOutputType#DETAILED} is used
*/
public void setExceptionOutputType(final ExceptionOutputType exceptionOutputType) {
if (exceptionOutputType == null) {
this.exceptionOutputType = ExceptionOutputType.DETAILED;
} else {
this.exceptionOutputType = exceptionOutputType;
}
}
/**
* Checks the exception output type and determines if detailed output should be written.
*
* @return {@code true} if detailed output should be written, otherwise {@code false}
*/
protected boolean isDetailedExceptionOutputType() {
final ExceptionOutputType exceptionOutputType = this.exceptionOutputType;
return exceptionOutputType == ExceptionOutputType.DETAILED ||
exceptionOutputType == ExceptionOutputType.DETAILED_AND_FORMATTED;
}
/**
* Checks the exception output type and determines if formatted output should be written. The formatted output is
* equivalent to {@link Throwable#printStackTrace()}.
*
* @return {@code true} if formatted exception output should be written, otherwise {@code false}
*/
protected boolean isFormattedExceptionOutputType() {
final ExceptionOutputType exceptionOutputType = this.exceptionOutputType;
return exceptionOutputType == ExceptionOutputType.FORMATTED ||
exceptionOutputType == ExceptionOutputType.DETAILED_AND_FORMATTED;
}
private void addException(final Generator generator, final Throwable throwable, final Map seen) throws Exception {
if (throwable == null) {
return;
}
if (seen.containsKey(throwable)) {
generator.addAttribute(getKey(Key.EXCEPTION_REFERENCE_ID), seen.get(throwable));
generator.startObject(getKey(Key.EXCEPTION_CIRCULAR_REFERENCE));
generator.add(getKey(Key.EXCEPTION_MESSAGE), throwable.getMessage());
generator.endObject(); // end circular reference
} else {
final int id = ++refId;
seen.put(throwable, id);
generator.addAttribute(getKey(Key.EXCEPTION_REFERENCE_ID), id);
generator.add(getKey(Key.EXCEPTION_TYPE), throwable.getClass().getName());
generator.add(getKey(Key.EXCEPTION_MESSAGE), throwable.getMessage());
final StackTraceElement[] elements = throwable.getStackTrace();
addStackTraceElements(generator, elements);
// Render the suppressed messages
final Throwable[] suppressed = throwable.getSuppressed();
if (suppressed != null && suppressed.length > 0) {
generator.startArray(getKey(Key.EXCEPTION_SUPPRESSED));
for (Throwable s : suppressed) {
if (generator.wrapArrays()) {
generator.startObject(getKey(Key.EXCEPTION));
} else {
generator.startObject(null);
}
addException(generator, s, seen);
generator.endObject(); // end exception
}
generator.endArray();
}
// Render the cause
final Throwable cause = throwable.getCause();
if (cause != null) {
generator.startObject(getKey(Key.EXCEPTION_CAUSED_BY));
generator.startObject(getKey(Key.EXCEPTION));
addException(generator, cause, seen);
generator.endObject();
generator.endObject(); // end exception
}
}
}
private void addStackTraceElements(final Generator generator, final StackTraceElement[] elements) throws Exception {
generator.startArray(getKey(Key.EXCEPTION_FRAMES));
for (StackTraceElement e : elements) {
if (generator.wrapArrays()) {
generator.startObject(getKey(Key.EXCEPTION_FRAME));
} else {
generator.startObject(null);
}
generator.add(getKey(Key.EXCEPTION_FRAME_CLASS), e.getClassName());
generator.add(getKey(Key.EXCEPTION_FRAME_METHOD), e.getMethodName());
final int line = e.getLineNumber();
if (line >= 0) {
generator.add(getKey(Key.EXCEPTION_FRAME_LINE), e.getLineNumber());
}
generator.endObject(); // end exception object
}
generator.endArray(); // end array
}
private static boolean isNotNullOrEmpty(final String value) {
return value != null && !value.isEmpty();
}
private static boolean isNotNullOrEmpty(final Collection> value) {
return value != null && !value.isEmpty();
}
/**
* A generator used to create the structured output.
*/
@SuppressWarnings("UnusedReturnValue")
protected interface Generator {
/**
* Initial method invoked at the start of the generation.
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
default Generator begin() throws Exception {
return this;
}
/**
* Writes an integer value.
*
* @param key they key
* @param value the value
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
default Generator add(final String key, final int value) throws Exception {
add(key, Integer.toString(value));
return this;
}
/**
* Writes a long value.
*
* @param key they key
* @param value the value
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
default Generator add(final String key, final long value) throws Exception {
add(key, Long.toString(value));
return this;
}
/**
* Writes a map value
*
* @param key the key for the map
* @param value the map
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
Generator add(String key, Map value) throws Exception;
/**
* Writes a string value.
*
* @param key the key for the value
* @param value the string value
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
Generator add(String key, String value) throws Exception;
/**
* Adds the meta data to the structured format.
*
* By default this processes the map and uses {@link #add(String, String)} to add entries.
*
*
* @param metaData the matp of the meta data, cannot be {@code null}
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
default Generator addMetaData(final Map metaData) throws Exception {
for (String key : metaData.keySet()) {
add(key, metaData.get(key));
}
return this;
}
/**
* Writes the start of an object.
*
* If the {@link #wrapArrays()} returns {@code false} the key may be {@code null} and implementations should
* handle this.
*
*
* @param key they key for the object, or {@code null} if this object was
* {@linkplain #startArray(String) started in an array} and the {@link #wrapArrays()} is
* {@code false}
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
Generator startObject(String key) throws Exception;
/**
* Writes an end to the object.
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
Generator endObject() throws Exception;
/**
* Writes the start of an array. This defaults to {@link #startObject(String)} for convenience of generators
* that don't have a specific type for arrays.
*
* @param key they key for the array
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
default Generator startArray(String key) throws Exception {
return startObject(key);
}
/**
* Writes an end for an array. This defaults to {@link #endObject()} for convenience of generators that don't
* have a specific type for arrays.
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
default Generator endArray() throws Exception {
return endObject();
}
/**
* Writes an attribute.
*
* By default this uses the {@link #add(String, int)} method to add the attribute. If a formatter requires
* special handling for attributes, for example an attribute on an XML element, this method can be overridden.
*
*
* @param name the name of the attribute
* @param value the value of the attribute
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
default Generator addAttribute(final String name, final int value) throws Exception {
return add(name, value);
}
/**
* Writes an attribute.
*
* By default this uses the {@link #add(String, String)} method to add the attribute. If a formatter requires
* special handling for attributes, for example an attribute on an XML element, this method can be overridden.
*
*
* @param name the name of the attribute
* @param value the value of the attribute
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data
*/
default Generator addAttribute(final String name, final String value) throws Exception {
return add(name, value);
}
/**
* Writes any trailing data that's needed.
*
* @return the generator
*
* @throws Exception if an error occurs while adding the data during the build
*/
Generator end() throws Exception;
/**
* Indicates whether or not elements in an array should be wrapped or not. The default is {@code false}.
*
* @return {@code true} if elements should be wrapped, otherwise {@code false}
*/
default boolean wrapArrays() {
return false;
}
}
}