@ -19,23 +19,26 @@ package org.springframework.boot.logging.log4j2;
import java.math.BigDecimal ;
import java.math.BigDecimal ;
import java.util.Objects ;
import java.util.Objects ;
import java.util.Set ;
import java.util.Set ;
import java.util.function.Function ;
import java.util.function.BiConsumer ;
import java.util.regex.Pattern ;
import java.util.regex.Pattern ;
import org.apache.commons.logging.Log ;
import org.apache.commons.logging.LogFactory ;
import org.apache.logging.log4j.Level ;
import org.apache.logging.log4j.Level ;
import org.apache.logging.log4j.core.LogEvent ;
import org.apache.logging.log4j.core.LogEvent ;
import org.apache.logging.log4j.core.impl.ThrowableProxy ;
import org.apache.logging.log4j.core.net.Severity ;
import org.apache.logging.log4j.core.net.Severity ;
import org.apache.logging.log4j.core.time.Instant ;
import org.apache.logging.log4j.core.time.Instant ;
import org.apache.logging.log4j.message.Message ;
import org.apache.logging.log4j.message.Message ;
import org.apache.logging.log4j.util.ReadOnlyStringMap ;
import org.apache.logging.log4j.util.ReadOnlyStringMap ;
import org.springframework.boot.json.JsonWriter ;
import org.springframework.boot.json.JsonWriter ;
import org.springframework.boot.json.JsonWriter.WritableJson ;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat ;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat ;
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatService ;
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatService ;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter ;
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter ;
import org.springframework.boot.logging.structured.StructuredLogFormatter ;
import org.springframework.boot.logging.structured.StructuredLogFormatter ;
import org.springframework.core.env.Environment ;
import org.springframework.core.env.Environment ;
import org.springframework.core.log.LogMessage ;
import org.springframework.util.Assert ;
import org.springframework.util.Assert ;
import org.springframework.util.ObjectUtils ;
import org.springframework.util.ObjectUtils ;
@ -45,14 +48,17 @@ import org.springframework.util.ObjectUtils;
* 1 . 1 .
* 1 . 1 .
*
*
* @author Samuel Lissner
* @author Samuel Lissner
* @author Moritz Halbritter
* /
* /
class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructuredLogFormatter < LogEvent > {
class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructuredLogFormatter < LogEvent > {
private static final Log logger = LogFactory . getLog ( GraylogExtendedLogFormatStructuredLogFormatter . class ) ;
/ * *
/ * *
* Allowed characters in field names are any word character ( letter , number ,
* Allowed characters in field names are any word character ( letter , number ,
* underscore ) , dashes and dots .
* underscore ) , dashes and dots .
* /
* /
private static final Pattern FIELD_NAME_VALID_PATTERN = Pattern . compile ( "^[\\w\\ .\\-]*$" ) ;
private static final Pattern FIELD_NAME_VALID_PATTERN = Pattern . compile ( "^[\\w.\\-]*$" ) ;
/ * *
/ * *
* Every field been sent and prefixed with an underscore "_" will be treated as an
* Every field been sent and prefixed with an underscore "_" will be treated as an
@ -64,13 +70,7 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
* Libraries SHOULD not allow to send id as additional field ( "_id" ) . Graylog server
* Libraries SHOULD not allow to send id as additional field ( "_id" ) . Graylog server
* nodes omit this field automatically .
* nodes omit this field automatically .
* /
* /
private static final Set < String > ADDITIONAL_FIELD_ILLEGAL_KEYS = Set . of ( "_id" ) ;
private static final Set < String > ADDITIONAL_FIELD_ILLEGAL_KEYS = Set . of ( "id" , "_id" ) ;
/ * *
* Default format to be used for the ` full_message ` property when there is a throwable
* present in the log event .
* /
private static final String DEFAULT_FULL_MESSAGE_WITH_THROWABLE_FORMAT = "%s%n%n%s" ;
GraylogExtendedLogFormatStructuredLogFormatter ( Environment environment ) {
GraylogExtendedLogFormatStructuredLogFormatter ( Environment environment ) {
super ( ( members ) - > jsonMembers ( environment , members ) ) ;
super ( ( members ) - > jsonMembers ( environment , members ) ) ;
@ -78,40 +78,30 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
private static void jsonMembers ( Environment environment , JsonWriter . Members < LogEvent > members ) {
private static void jsonMembers ( Environment environment , JsonWriter . Members < LogEvent > members ) {
members . add ( "version" , "1.1" ) ;
members . add ( "version" , "1.1" ) ;
// note: a blank message will lead to a Graylog error as of Graylog v6.0.x. We are
// note: a blank message will lead to a Graylog error as of Graylog v6.0.x. We are
// ignoring this here.
// ignoring this here.
members . add ( "short_message" , LogEvent : : getMessage ) . as ( Message : : getFormattedMessage ) ;
members . add ( "short_message" , LogEvent : : getMessage ) . as ( Message : : getFormattedMessage ) ;
members . add ( "timestamp" , LogEvent : : getInstant )
members . add ( "timestamp" , LogEvent : : getInstant )
. as ( GraylogExtendedLogFormatStructuredLogFormatter : : formatTimeStamp ) ;
. as ( GraylogExtendedLogFormatStructuredLogFormatter : : formatTimeStamp ) ;
members . add ( "level" , GraylogExtendedLogFormatStructuredLogFormatter : : convertLevel ) ;
members . add ( "level" , GraylogExtendedLogFormatStructuredLogFormatter : : convertLevel ) ;
members . add ( "_level_name" , LogEvent : : getLevel ) . as ( Level : : name ) ;
members . add ( "_level_name" , LogEvent : : getLevel ) . as ( Level : : name ) ;
members . add ( "_process_pid" , environment . getProperty ( "spring.application.pid" , Long . class ) )
members . add ( "_process_pid" , environment . getProperty ( "spring.application.pid" , Long . class ) )
. when ( Objects : : nonNull ) ;
. when ( Objects : : nonNull ) ;
members . add ( "_process_thread_name" , LogEvent : : getThreadName ) ;
members . add ( "_process_thread_name" , LogEvent : : getThreadName ) ;
GraylogExtendedLogFormatService . get ( environment ) . jsonMembers ( members ) ;
GraylogExtendedLogFormatService . get ( environment ) . jsonMembers ( members ) ;
members . add ( "_log_logger" , LogEvent : : getLoggerName ) ;
members . add ( "_log_logger" , LogEvent : : getLoggerName ) ;
members . from ( LogEvent : : getContextData )
members . from ( LogEvent : : getContextData )
. whenNot ( ReadOnlyStringMap : : isEmpty )
. whenNot ( ReadOnlyStringMap : : isEmpty )
. usingPairs ( ( contextData , pairs ) - > contextData
. usingPairs ( ( contextData , pairs ) - > contextData
. forEach ( ( key , value ) - > pairs . accept ( makeAdditionalFieldName ( key ) , value ) ) ) ;
. forEach ( ( key , value ) - > createAdditionalField ( key , value , pairs ) ) ) ;
members . add ( ) . whenNotNull ( LogEvent : : getThrownProxy ) . usingMembers ( ( eventMembers ) - > {
members . add ( ) . whenNotNull ( LogEvent : : getThrownProxy ) . usingMembers ( ( eventMembers ) - > {
final Function < LogEvent , ThrowableProxy > throwableProxyGetter = LogEvent : : getThrownProxy ;
eventMembers . add ( "full_message" ,
eventMembers . add ( "full_message" ,
GraylogExtendedLogFormatStructuredLogFormatter : : formatFullMessageWithThrowable ) ;
GraylogExtendedLogFormatStructuredLogFormatter : : formatFullMessageWithThrowable ) ;
eventMembers . add ( "_error_type" , throwableProxyGetter . andThen ( ThrowableProxy : : getThrowable ) )
eventMembers . add ( "_error_type" , ( event ) - > event . getThrownProxy ( ) . getThrowable ( ) )
. whenNotNull ( )
. whenNotNull ( )
. as ( ObjectUtils : : nullSafeClassName ) ;
. as ( ObjectUtils : : nullSafeClassName ) ;
eventMembers . add ( "_error_stack_trace" ,
eventMembers . add ( "_error_stack_trace" , ( event ) - > event . getThrownProxy ( ) . getExtendedStackTraceAsString ( ) ) ;
throwableProxyGetter . andThen ( ThrowableProxy : : getExtendedStackTraceAsString ) ) ;
eventMembers . add ( "_error_message" , ( event ) - > event . getThrownProxy ( ) . getMessage ( ) ) ;
eventMembers . add ( "_error_message" , throwableProxyGetter . andThen ( ThrowableProxy : : getMessage ) ) ;
} ) ;
} ) ;
}
}
@ -123,8 +113,8 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
* ` Instant ` type but { @link org . apache . logging . log4j . core . time }
* ` Instant ` type but { @link org . apache . logging . log4j . core . time }
* @return the timestamp formatted as string with millisecond precision
* @return the timestamp formatted as string with millisecond precision
* /
* /
private static double formatTimeStamp ( final Instant timeStamp ) {
private static WritableJson formatTimeStamp ( Instant timeStamp ) {
return new BigDecimal ( timeStamp . getEpochMillisecond ( ) ) . movePointLeft ( 3 ) . doubleValue ( ) ;
return ( out ) - > out . append ( new BigDecimal ( timeStamp . getEpochMillisecond ( ) ) . movePointLeft ( 3 ) . toPlainString ( ) ) ;
}
}
/ * *
/ * *
@ -133,30 +123,27 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
* @return an integer representing the syslog log level code
* @return an integer representing the syslog log level code
* @see Severity class from Log4j2 which contains the conversion logic
* @see Severity class from Log4j2 which contains the conversion logic
* /
* /
private static int convertLevel ( final LogEvent event ) {
private static int convertLevel ( LogEvent event ) {
return Severity . getSeverity ( event . getLevel ( ) ) . getCode ( ) ;
return Severity . getSeverity ( event . getLevel ( ) ) . getCode ( ) ;
}
}
private static String formatFullMessageWithThrowable ( final LogEvent event ) {
private static String formatFullMessageWithThrowable ( LogEvent event ) {
return String . format ( DEFAULT_FULL_MESSAGE_WITH_THROWABLE_FORMAT , event . getMessage ( ) . getFormattedMessage ( ) ,
return event . getMessage ( ) . getFormattedMessage ( ) + "\n\n"
event . getThrownProxy ( ) . getExtendedStackTraceAsString ( ) ) ;
+ event . getThrownProxy ( ) . getExtendedStackTraceAsString ( ) ;
}
}
private static String makeAdditionalFieldName ( String fieldName ) {
private static void createAdditionalField ( String fieldName , Object value , BiConsumer < Object , Object > pairs ) {
Assert . notNull ( fieldName , "fieldName must not be null" ) ;
Assert . notNull ( fieldName , "fieldName must not be null" ) ;
Assert . isTrue ( FIELD_NAME_VALID_PATTERN . matcher ( fieldName ) . matches ( ) ,
if ( ! FIELD_NAME_VALID_PATTERN . matcher ( fieldName ) . matches ( ) ) {
( ) - > String . format ( "fieldName must be a valid according to GELF standard. [fieldName=%s]" , fieldName ) ) ;
logger . warn ( LogMessage . format ( "'%s' is not a valid field name according to GELF standard" , fieldName ) ) ;
Assert . isTrue ( ! ADDITIONAL_FIELD_ILLEGAL_KEYS . contains ( fieldName ) , ( ) - > String . format (
return ;
"fieldName must not be an illegal additional field key according to GELF standard. [fieldName=%s]" ,
fieldName ) ) ;
if ( fieldName . startsWith ( ADDITIONAL_FIELD_PREFIX ) ) {
// No need to prepend the `ADDITIONAL_FIELD_PREFIX` in case the caller already
// has prepended the prefix.
return fieldName ;
}
}
if ( ADDITIONAL_FIELD_ILLEGAL_KEYS . contains ( fieldName ) ) {
return ADDITIONAL_FIELD_PREFIX + fieldName ;
logger . warn ( LogMessage . format ( "'%s' is an illegal field name according to GELF standard" , fieldName ) ) ;
return ;
}
String key = ( fieldName . startsWith ( ADDITIONAL_FIELD_PREFIX ) ) ? fieldName : ADDITIONAL_FIELD_PREFIX + fieldName ;
pairs . accept ( key , value ) ;
}
}
}
}