This commit is contained in:
Phillip Webb
2024-09-11 13:56:25 -07:00
parent a418943cbc
commit 06e3f37afd
8 changed files with 109 additions and 74 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -454,6 +454,7 @@ To enable structured logging, set the property configprop:logging.structured.for
[[features.logging.structured.ecs]]
=== Elastic Common Schema
https://www.elastic.co/guide/en/ecs/8.11/ecs-reference.html[Elastic Common Schema] is a JSON based logging format.
To enable the Elastic Common Schema log format, set the appropriate `format` property to `ecs`:
@@ -499,6 +500,7 @@ NOTE: configprop:logging.structured.ecs.service.version[] will default to config
[[features.logging.structured.gelf]]
=== Graylog Extended Log Format (GELF)
https://go2docs.graylog.org/current/getting_in_log_data/gelf.html[Graylog Extended Log Format] is a JSON based logging format for the Graylog log analytics platform.
To enable the Graylog Extended Log Format, set the appropriate `format` property to `gelf`:

View File

@@ -102,12 +102,11 @@ final class ConfigurationPropertiesBeanRegistrar {
static BeanDefinitionHolder applyScopedProxyMode(ScopeMetadata metadata, BeanDefinitionHolder definition,
BeanDefinitionRegistry registry) {
ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
return definition;
ScopedProxyMode mode = metadata.getScopedProxyMode();
if (mode != ScopedProxyMode.NO) {
return ScopedProxyUtils.createScopedProxy(definition, registry, mode == ScopedProxyMode.TARGET_CLASS);
}
boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
return ScopedProxyUtils.createScopedProxy(definition, registry, proxyTargetClass);
return definition;
}
}

View File

@@ -32,6 +32,7 @@ import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.json.JsonWriter.WritableJson;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatService;
@@ -41,6 +42,7 @@ import org.springframework.core.env.Environment;
import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Log4j2 {@link StructuredLogFormatter} for
@@ -60,12 +62,6 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
*/
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
* additional field.
*/
private static final String ADDITIONAL_FIELD_PREFIX = "_";
/**
* Libraries SHOULD not allow to send id as additional field ("_id"). Graylog server
* nodes omit this field automatically.
@@ -78,9 +74,8 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
private static void jsonMembers(Environment environment, JsonWriter.Members<LogEvent> members) {
members.add("version", "1.1");
// note: a blank message will lead to a Graylog error as of Graylog v6.0.x. We are
// ignoring this here.
members.add("short_message", LogEvent::getMessage).as(Message::getFormattedMessage);
members.add("short_message", LogEvent::getMessage)
.as(GraylogExtendedLogFormatStructuredLogFormatter::getMessageText);
members.add("timestamp", LogEvent::getInstant)
.as(GraylogExtendedLogFormatStructuredLogFormatter::formatTimeStamp);
members.add("level", GraylogExtendedLogFormatStructuredLogFormatter::convertLevel);
@@ -92,25 +87,23 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
members.add("_log_logger", LogEvent::getLoggerName);
members.from(LogEvent::getContextData)
.whenNot(ReadOnlyStringMap::isEmpty)
.usingPairs((contextData, pairs) -> contextData
.forEach((key, value) -> createAdditionalField(key, value, pairs)));
members.add().whenNotNull(LogEvent::getThrownProxy).usingMembers((eventMembers) -> {
eventMembers.add("full_message",
GraylogExtendedLogFormatStructuredLogFormatter::formatFullMessageWithThrowable);
eventMembers.add("_error_type", (event) -> event.getThrownProxy().getThrowable())
.whenNotNull()
.as(ObjectUtils::nullSafeClassName);
eventMembers.add("_error_stack_trace", (event) -> event.getThrownProxy().getExtendedStackTraceAsString());
eventMembers.add("_error_message", (event) -> event.getThrownProxy().getMessage());
});
.usingPairs(GraylogExtendedLogFormatStructuredLogFormatter::createAdditionalFields);
members.add()
.whenNotNull(LogEvent::getThrownProxy)
.usingMembers(GraylogExtendedLogFormatStructuredLogFormatter::throwableMembers);
}
private static String getMessageText(Message message) {
// Always return text as a blank message will lead to a error as of Graylog v6
String formattedMessage = message.getFormattedMessage();
return (!StringUtils.hasText(formattedMessage)) ? "(blank)" : formattedMessage;
}
/**
* GELF requires "seconds since UNIX epoch with optional <b>decimal places for
* milliseconds</b>". To comply with this requirement, we format a POSIX timestamp
* with millisecond precision as e.g. "1725459730385" -> "1725459730.385"
* @param timeStamp the timestamp of the log message. Note it is not the standard Java
* `Instant` type but {@link org.apache.logging.log4j.core.time}
* @param timeStamp the timestamp of the log message.
* @return the timestamp formatted as string with millisecond precision
*/
private static WritableJson formatTimeStamp(Instant timeStamp) {
@@ -127,23 +120,39 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
return Severity.getSeverity(event.getLevel()).getCode();
}
private static void throwableMembers(Members<LogEvent> members) {
members.add("full_message", GraylogExtendedLogFormatStructuredLogFormatter::formatFullMessageWithThrowable);
members.add("_error_type", (event) -> event.getThrownProxy().getThrowable())
.whenNotNull()
.as(ObjectUtils::nullSafeClassName);
members.add("_error_stack_trace", (event) -> event.getThrownProxy().getExtendedStackTraceAsString());
members.add("_error_message", (event) -> event.getThrownProxy().getMessage());
}
private static String formatFullMessageWithThrowable(LogEvent event) {
return event.getMessage().getFormattedMessage() + "\n\n"
+ event.getThrownProxy().getExtendedStackTraceAsString();
}
private static void createAdditionalField(String fieldName, Object value, BiConsumer<Object, Object> pairs) {
Assert.notNull(fieldName, "fieldName must not be null");
if (!FIELD_NAME_VALID_PATTERN.matcher(fieldName).matches()) {
logger.warn(LogMessage.format("'%s' is not a valid field name according to GELF standard", fieldName));
private static void createAdditionalFields(ReadOnlyStringMap contextData, BiConsumer<Object, Object> pairs) {
contextData.forEach((name, value) -> createAdditionalField(name, value, pairs));
}
private static void createAdditionalField(String name, Object value, BiConsumer<Object, Object> pairs) {
Assert.notNull(name, "fieldName must not be null");
if (!FIELD_NAME_VALID_PATTERN.matcher(name).matches()) {
logger.warn(LogMessage.format("'%s' is not a valid field name according to GELF standard", name));
return;
}
if (ADDITIONAL_FIELD_ILLEGAL_KEYS.contains(fieldName)) {
logger.warn(LogMessage.format("'%s' is an illegal field name according to GELF standard", fieldName));
if (ADDITIONAL_FIELD_ILLEGAL_KEYS.contains(name)) {
logger.warn(LogMessage.format("'%s' is an illegal field name according to GELF standard", name));
return;
}
String key = (fieldName.startsWith(ADDITIONAL_FIELD_PREFIX)) ? fieldName : ADDITIONAL_FIELD_PREFIX + fieldName;
pairs.accept(key, value);
pairs.accept(asAdditionalFieldName(name), value);
}
private static Object asAdditionalFieldName(String name) {
return (!name.startsWith("_")) ? "_" + name : name;
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.boot.logging.logback;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
@@ -28,8 +29,10 @@ import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.util.LevelToSyslogSeverity;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.slf4j.event.KeyValuePair;
import org.springframework.boot.json.JsonWriter;
import org.springframework.boot.json.JsonWriter.Members;
import org.springframework.boot.json.JsonWriter.WritableJson;
import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatService;
@@ -39,6 +42,7 @@ import org.springframework.core.env.Environment;
import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* Logback {@link StructuredLogFormatter} for
@@ -58,12 +62,6 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
*/
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
* additional field.
*/
private static final String ADDITIONAL_FIELD_PREFIX = "_";
/**
* Libraries SHOULD not allow to send id as additional field ("_id"). Graylog server
* nodes omit this field automatically.
@@ -78,9 +76,8 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
private static void jsonMembers(Environment environment, ThrowableProxyConverter throwableProxyConverter,
JsonWriter.Members<ILoggingEvent> members) {
members.add("version", "1.1");
// note: a blank message will lead to a Graylog error as of Graylog v6.0.x. We are
// ignoring this here.
members.add("short_message", ILoggingEvent::getFormattedMessage);
members.add("short_message", ILoggingEvent::getFormattedMessage)
.as(GraylogExtendedLogFormatStructuredLogFormatter::getMessageText);
members.add("timestamp", ILoggingEvent::getTimeStamp)
.as(GraylogExtendedLogFormatStructuredLogFormatter::formatTimeStamp);
members.add("level", LevelToSyslogSeverity::convert);
@@ -95,15 +92,15 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
.usingPairs((mdc, pairs) -> mdc.forEach((key, value) -> createAdditionalField(key, value, pairs)));
members.from(ILoggingEvent::getKeyValuePairs)
.when((keyValuePairs) -> !CollectionUtils.isEmpty(keyValuePairs))
.usingPairs((keyValuePairs, pairs) -> keyValuePairs
.forEach((keyValuePair) -> createAdditionalField(keyValuePair.key, keyValuePair.value, pairs)));
members.add().whenNotNull(ILoggingEvent::getThrowableProxy).usingMembers((throwableMembers) -> {
throwableMembers.add("full_message",
(event) -> formatFullMessageWithThrowable(throwableProxyConverter, event));
throwableMembers.add("_error_type", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getClassName);
throwableMembers.add("_error_stack_trace", throwableProxyConverter::convert);
throwableMembers.add("_error_message", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getMessage);
});
.usingPairs(GraylogExtendedLogFormatStructuredLogFormatter::createAdditionalField);
members.add()
.whenNotNull(ILoggingEvent::getThrowableProxy)
.usingMembers((throwableMembers) -> throwableMembers(throwableMembers, throwableProxyConverter));
}
private static String getMessageText(String formattedMessage) {
// Always return text as a blank message will lead to a error as of Graylog v6
return (!StringUtils.hasText(formattedMessage)) ? "(blank)" : formattedMessage;
}
/**
@@ -117,23 +114,38 @@ class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructure
return (out) -> out.append(new BigDecimal(timeStamp).movePointLeft(3).toPlainString());
}
private static void throwableMembers(Members<ILoggingEvent> members,
ThrowableProxyConverter throwableProxyConverter) {
members.add("full_message", (event) -> formatFullMessageWithThrowable(throwableProxyConverter, event));
members.add("_error_type", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getClassName);
members.add("_error_stack_trace", throwableProxyConverter::convert);
members.add("_error_message", ILoggingEvent::getThrowableProxy).as(IThrowableProxy::getMessage);
}
private static String formatFullMessageWithThrowable(ThrowableProxyConverter throwableProxyConverter,
ILoggingEvent event) {
return event.getFormattedMessage() + "\n\n" + throwableProxyConverter.convert(event);
}
private static void createAdditionalField(String key, Object value, BiConsumer<Object, Object> pairs) {
Assert.notNull(key, "fieldName must not be null");
if (!FIELD_NAME_VALID_PATTERN.matcher(key).matches()) {
logger.warn(LogMessage.format("'%s' is not a valid field name according to GELF standard", key));
private static void createAdditionalField(List<KeyValuePair> keyValuePairs, BiConsumer<Object, Object> pairs) {
keyValuePairs.forEach((keyValuePair) -> createAdditionalField(keyValuePair.key, keyValuePair.value, pairs));
}
private static void createAdditionalField(String name, Object value, BiConsumer<Object, Object> pairs) {
Assert.notNull(name, "fieldName must not be null");
if (!FIELD_NAME_VALID_PATTERN.matcher(name).matches()) {
logger.warn(LogMessage.format("'%s' is not a valid field name according to GELF standard", name));
return;
}
if (ADDITIONAL_FIELD_ILLEGAL_KEYS.contains(key)) {
logger.warn(LogMessage.format("'%s' is an illegal field name according to GELF standard", key));
if (ADDITIONAL_FIELD_ILLEGAL_KEYS.contains(name)) {
logger.warn(LogMessage.format("'%s' is an illegal field name according to GELF standard", name));
return;
}
String keyWithPrefix = (key.startsWith(ADDITIONAL_FIELD_PREFIX)) ? key : ADDITIONAL_FIELD_PREFIX + key;
pairs.accept(keyWithPrefix, value);
pairs.accept(asAdditionalFieldName(name), value);
}
private static Object asAdditionalFieldName(String name) {
return (!name.startsWith("_")) ? "_" + name : name;
}
}

View File

@@ -28,6 +28,7 @@ import org.springframework.boot.logging.structured.CommonStructuredLogFormat;
import org.springframework.boot.logging.structured.StructuredLogFormatter;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory;
import org.springframework.boot.logging.structured.StructuredLogFormatterFactory.CommonFormatters;
import org.springframework.boot.util.Instantiator;
import org.springframework.boot.util.Instantiator.AvailableParameters;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
@@ -79,16 +80,29 @@ public class StructuredLogEncoder extends EncoderBase<ILoggingEvent> {
}
private void addCommonFormatters(CommonFormatters<ILoggingEvent> commonFormatters) {
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA,
(instantiator) -> new ElasticCommonSchemaStructuredLogFormatter(instantiator.getArg(Environment.class),
instantiator.getArg(ThrowableProxyConverter.class)));
commonFormatters
.add(CommonStructuredLogFormat.GRAYLOG_EXTENDED_LOG_FORMAT,
(instantiator) -> new GraylogExtendedLogFormatStructuredLogFormatter(
instantiator.getArg(Environment.class),
instantiator.getArg(ThrowableProxyConverter.class)));
commonFormatters.add(CommonStructuredLogFormat.LOGSTASH, (instantiator) -> new LogstashStructuredLogFormatter(
instantiator.getArg(ThrowableProxyConverter.class)));
commonFormatters.add(CommonStructuredLogFormat.ELASTIC_COMMON_SCHEMA, this::createEcsFormatter);
commonFormatters.add(CommonStructuredLogFormat.GRAYLOG_EXTENDED_LOG_FORMAT, this::createGraylogFormatter);
commonFormatters.add(CommonStructuredLogFormat.LOGSTASH, this::createLogstashFormatter);
}
private StructuredLogFormatter<ILoggingEvent> createEcsFormatter(
Instantiator<StructuredLogFormatter<ILoggingEvent>> instantiator) {
Environment environment = instantiator.getArg(Environment.class);
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
return new ElasticCommonSchemaStructuredLogFormatter(environment, throwableProxyConverter);
}
private StructuredLogFormatter<ILoggingEvent> createGraylogFormatter(
Instantiator<StructuredLogFormatter<ILoggingEvent>> instantiator) {
Environment environment = instantiator.getArg(Environment.class);
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
return new GraylogExtendedLogFormatStructuredLogFormatter(environment, throwableProxyConverter);
}
private StructuredLogFormatter<ILoggingEvent> createLogstashFormatter(
Instantiator<StructuredLogFormatter<ILoggingEvent>> instantiator) {
ThrowableProxyConverter throwableProxyConverter = instantiator.getArg(ThrowableProxyConverter.class);
return new LogstashStructuredLogFormatter(throwableProxyConverter);
}
@Override

View File

@@ -48,7 +48,6 @@ public record GraylogExtendedLogFormatService(String name, String version) {
* @param members the members to add to
*/
public void jsonMembers(JsonWriter.Members<?> members) {
// note "host" is a field name prescribed by GELF
members.add("host", this::name).whenHasLength();
members.add("_service_version", this::version).whenHasLength();
}
@@ -65,4 +64,5 @@ public record GraylogExtendedLogFormatService(String name, String version) {
.orElse(NONE)
.withDefaults(environment);
}
}

View File

@@ -134,7 +134,6 @@ class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStruct
.formatted());
assertThat(deserialized)
.containsAllEntriesOf(map("_error_type", "java.lang.RuntimeException", "_error_message", "Boom"));
assertThat(stackTrace).startsWith(
"java.lang.RuntimeException: Boom%n\tat org.springframework.boot.logging.logback.GraylogExtendedLogFormatStructuredLogFormatterTests.shouldFormatException"
.formatted());