Browse Source
* pr/42158: Polish "Add Graylog Extended Log Format (GELF) for structured logging" Add Graylog Extended Log Format (GELF) for structured logging Closes gh-42158pull/42277/head
13 changed files with 799 additions and 3 deletions
@ -0,0 +1,149 @@
@@ -0,0 +1,149 @@
|
||||
/* |
||||
* 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://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.springframework.boot.logging.log4j2; |
||||
|
||||
import java.math.BigDecimal; |
||||
import java.util.Objects; |
||||
import java.util.Set; |
||||
import java.util.function.BiConsumer; |
||||
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.core.LogEvent; |
||||
import org.apache.logging.log4j.core.net.Severity; |
||||
import org.apache.logging.log4j.core.time.Instant; |
||||
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.WritableJson; |
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat; |
||||
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatService; |
||||
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter; |
||||
import org.springframework.boot.logging.structured.StructuredLogFormatter; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.core.log.LogMessage; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ObjectUtils; |
||||
|
||||
/** |
||||
* Log4j2 {@link StructuredLogFormatter} for |
||||
* {@link CommonStructuredLogFormat#GRAYLOG_EXTENDED_LOG_FORMAT}. Supports GELF version |
||||
* 1.1. |
||||
* |
||||
* @author Samuel Lissner |
||||
* @author Moritz Halbritter |
||||
*/ |
||||
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, |
||||
* underscore), dashes and dots. |
||||
*/ |
||||
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. |
||||
*/ |
||||
private static final Set<String> ADDITIONAL_FIELD_ILLEGAL_KEYS = Set.of("id", "_id"); |
||||
|
||||
GraylogExtendedLogFormatStructuredLogFormatter(Environment environment) { |
||||
super((members) -> jsonMembers(environment, members)); |
||||
} |
||||
|
||||
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("timestamp", LogEvent::getInstant) |
||||
.as(GraylogExtendedLogFormatStructuredLogFormatter::formatTimeStamp); |
||||
members.add("level", GraylogExtendedLogFormatStructuredLogFormatter::convertLevel); |
||||
members.add("_level_name", LogEvent::getLevel).as(Level::name); |
||||
members.add("_process_pid", environment.getProperty("spring.application.pid", Long.class)) |
||||
.when(Objects::nonNull); |
||||
members.add("_process_thread_name", LogEvent::getThreadName); |
||||
GraylogExtendedLogFormatService.get(environment).jsonMembers(members); |
||||
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()); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 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} |
||||
* @return the timestamp formatted as string with millisecond precision |
||||
*/ |
||||
private static WritableJson formatTimeStamp(Instant timeStamp) { |
||||
return (out) -> out.append(new BigDecimal(timeStamp.getEpochMillisecond()).movePointLeft(3).toPlainString()); |
||||
} |
||||
|
||||
/** |
||||
* Converts the log4j2 event level to the Syslog event level code. |
||||
* @param event the log event |
||||
* @return an integer representing the syslog log level code |
||||
* @see Severity class from Log4j2 which contains the conversion logic |
||||
*/ |
||||
private static int convertLevel(LogEvent event) { |
||||
return Severity.getSeverity(event.getLevel()).getCode(); |
||||
} |
||||
|
||||
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)); |
||||
return; |
||||
} |
||||
if (ADDITIONAL_FIELD_ILLEGAL_KEYS.contains(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); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,139 @@
@@ -0,0 +1,139 @@
|
||||
/* |
||||
* 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://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.springframework.boot.logging.logback; |
||||
|
||||
import java.math.BigDecimal; |
||||
import java.util.Objects; |
||||
import java.util.Set; |
||||
import java.util.function.BiConsumer; |
||||
import java.util.regex.Pattern; |
||||
|
||||
import ch.qos.logback.classic.pattern.ThrowableProxyConverter; |
||||
import ch.qos.logback.classic.spi.ILoggingEvent; |
||||
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.springframework.boot.json.JsonWriter; |
||||
import org.springframework.boot.json.JsonWriter.WritableJson; |
||||
import org.springframework.boot.logging.structured.CommonStructuredLogFormat; |
||||
import org.springframework.boot.logging.structured.GraylogExtendedLogFormatService; |
||||
import org.springframework.boot.logging.structured.JsonWriterStructuredLogFormatter; |
||||
import org.springframework.boot.logging.structured.StructuredLogFormatter; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.core.log.LogMessage; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.CollectionUtils; |
||||
|
||||
/** |
||||
* Logback {@link StructuredLogFormatter} for |
||||
* {@link CommonStructuredLogFormat#GRAYLOG_EXTENDED_LOG_FORMAT}. Supports GELF version |
||||
* 1.1. |
||||
* |
||||
* @author Samuel Lissner |
||||
* @author Moritz Halbritter |
||||
*/ |
||||
class GraylogExtendedLogFormatStructuredLogFormatter extends JsonWriterStructuredLogFormatter<ILoggingEvent> { |
||||
|
||||
private static final Log logger = LogFactory.getLog(GraylogExtendedLogFormatStructuredLogFormatter.class); |
||||
|
||||
/** |
||||
* Allowed characters in field names are any word character (letter, number, |
||||
* underscore), dashes and dots. |
||||
*/ |
||||
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. |
||||
*/ |
||||
private static final Set<String> ADDITIONAL_FIELD_ILLEGAL_KEYS = Set.of("id", "_id"); |
||||
|
||||
GraylogExtendedLogFormatStructuredLogFormatter(Environment environment, |
||||
ThrowableProxyConverter throwableProxyConverter) { |
||||
super((members) -> jsonMembers(environment, throwableProxyConverter, members)); |
||||
} |
||||
|
||||
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("timestamp", ILoggingEvent::getTimeStamp) |
||||
.as(GraylogExtendedLogFormatStructuredLogFormatter::formatTimeStamp); |
||||
members.add("level", LevelToSyslogSeverity::convert); |
||||
members.add("_level_name", ILoggingEvent::getLevel); |
||||
members.add("_process_pid", environment.getProperty("spring.application.pid", Long.class)) |
||||
.when(Objects::nonNull); |
||||
members.add("_process_thread_name", ILoggingEvent::getThreadName); |
||||
GraylogExtendedLogFormatService.get(environment).jsonMembers(members); |
||||
members.add("_log_logger", ILoggingEvent::getLoggerName); |
||||
members.from(ILoggingEvent::getMDCPropertyMap) |
||||
.when((mdc) -> !CollectionUtils.isEmpty(mdc)) |
||||
.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); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* 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 |
||||
* @return the timestamp formatted as string with millisecond precision |
||||
*/ |
||||
private static WritableJson formatTimeStamp(long timeStamp) { |
||||
return (out) -> out.append(new BigDecimal(timeStamp).movePointLeft(3).toPlainString()); |
||||
} |
||||
|
||||
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)); |
||||
return; |
||||
} |
||||
if (ADDITIONAL_FIELD_ILLEGAL_KEYS.contains(key)) { |
||||
logger.warn(LogMessage.format("'%s' is an illegal field name according to GELF standard", key)); |
||||
return; |
||||
} |
||||
String keyWithPrefix = (key.startsWith(ADDITIONAL_FIELD_PREFIX)) ? key : ADDITIONAL_FIELD_PREFIX + key; |
||||
pairs.accept(keyWithPrefix, value); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
|
||||
/* |
||||
* 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://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.springframework.boot.logging.structured; |
||||
|
||||
import org.springframework.boot.context.properties.bind.Binder; |
||||
import org.springframework.boot.json.JsonWriter; |
||||
import org.springframework.core.env.Environment; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* Service details for Graylog Extended Log Format structured logging. |
||||
* |
||||
* @param name the application name |
||||
* @param version the version of the application |
||||
* @author Samuel Lissner |
||||
* @since 3.4.0 |
||||
*/ |
||||
public record GraylogExtendedLogFormatService(String name, String version) { |
||||
|
||||
static final GraylogExtendedLogFormatService NONE = new GraylogExtendedLogFormatService(null, null); |
||||
|
||||
private GraylogExtendedLogFormatService withDefaults(Environment environment) { |
||||
String name = withFallbackProperty(environment, this.name, "spring.application.name"); |
||||
String version = withFallbackProperty(environment, this.version, "spring.application.version"); |
||||
return new GraylogExtendedLogFormatService(name, version); |
||||
} |
||||
|
||||
private String withFallbackProperty(Environment environment, String value, String property) { |
||||
return (!StringUtils.hasLength(value)) ? environment.getProperty(property) : value; |
||||
} |
||||
|
||||
/** |
||||
* Add {@link JsonWriter} members for the service. |
||||
* @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(); |
||||
} |
||||
|
||||
/** |
||||
* Return a new {@link GraylogExtendedLogFormatService} from bound from properties in |
||||
* the given {@link Environment}. |
||||
* @param environment the source environment |
||||
* @return a new {@link GraylogExtendedLogFormatService} instance |
||||
*/ |
||||
public static GraylogExtendedLogFormatService get(Environment environment) { |
||||
return Binder.get(environment) |
||||
.bind("logging.structured.gelf.service", GraylogExtendedLogFormatService.class) |
||||
.orElse(NONE) |
||||
.withDefaults(environment); |
||||
} |
||||
} |
||||
@ -0,0 +1,144 @@
@@ -0,0 +1,144 @@
|
||||
/* |
||||
* 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://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.springframework.boot.logging.log4j2; |
||||
|
||||
import java.util.Map; |
||||
|
||||
import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap; |
||||
import org.apache.logging.log4j.core.impl.MutableLogEvent; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
||||
import org.springframework.boot.testsupport.system.CapturedOutput; |
||||
import org.springframework.boot.testsupport.system.OutputCaptureExtension; |
||||
import org.springframework.mock.env.MockEnvironment; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link GraylogExtendedLogFormatStructuredLogFormatter}. |
||||
* |
||||
* @author Samuel Lissner |
||||
* @author Moritz Halbritter |
||||
*/ |
||||
@ExtendWith(OutputCaptureExtension.class) |
||||
class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStructuredLoggingTests { |
||||
|
||||
private GraylogExtendedLogFormatStructuredLogFormatter formatter; |
||||
|
||||
@BeforeEach |
||||
void setUp() { |
||||
MockEnvironment environment = new MockEnvironment(); |
||||
environment.setProperty("logging.structured.gelf.service.name", "name"); |
||||
environment.setProperty("logging.structured.gelf.service.version", "1.0.0"); |
||||
environment.setProperty("spring.application.pid", "1"); |
||||
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(environment); |
||||
} |
||||
|
||||
@Test |
||||
void shouldFormat() { |
||||
MutableLogEvent event = createEvent(); |
||||
event.setContextData(new JdkMapAdapterStringMap(Map.of("mdc-1", "mdc-v-1"), true)); |
||||
String json = this.formatter.format(event); |
||||
assertThat(json).endsWith("\n"); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf( |
||||
map("version", "1.1", "host", "name", "timestamp", 1719910193.0, "level", 6, "_level_name", "INFO", |
||||
"_process_pid", 1, "_process_thread_name", "main", "_service_version", "1.0.0", "_log_logger", |
||||
"org.example.Test", "short_message", "message", "_mdc-1", "mdc-v-1")); |
||||
} |
||||
|
||||
@Test |
||||
void shouldFormatMillisecondsInTimestamp() { |
||||
MutableLogEvent event = createEvent(); |
||||
event.setTimeMillis(1719910193123L); |
||||
String json = this.formatter.format(event); |
||||
assertThat(json).contains("\"timestamp\":1719910193.123"); |
||||
assertThat(json).endsWith("\n"); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("version", "1.1", "host", "name", "timestamp", |
||||
1719910193.123, "level", 6, "_level_name", "INFO", "_process_pid", 1, "_process_thread_name", "main", |
||||
"_service_version", "1.0.0", "_log_logger", "org.example.Test", "short_message", "message")); |
||||
} |
||||
|
||||
@Test |
||||
void shouldNotAllowInvalidFieldNames(CapturedOutput output) { |
||||
MutableLogEvent event = createEvent(); |
||||
event.setContextData(new JdkMapAdapterStringMap(Map.of("/", "value"), true)); |
||||
String json = this.formatter.format(event); |
||||
assertThat(json).endsWith("\n"); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("version", "1.1", "host", "name", "timestamp", |
||||
1719910193.0, "level", 6, "_level_name", "INFO", "_process_pid", 1, "_process_thread_name", "main", |
||||
"_service_version", "1.0.0", "_log_logger", "org.example.Test", "short_message", "message")); |
||||
assertThat(output).contains("'/' is not a valid field name according to GELF standard"); |
||||
} |
||||
|
||||
@Test |
||||
void shouldNotAllowIllegalFieldNames(CapturedOutput output) { |
||||
MutableLogEvent event = createEvent(); |
||||
event.setContextData(new JdkMapAdapterStringMap(Map.of("id", "1"), true)); |
||||
String json = this.formatter.format(event); |
||||
assertThat(json).endsWith("\n"); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("version", "1.1", "host", "name", "timestamp", |
||||
1719910193.0, "level", 6, "_level_name", "INFO", "_process_pid", 1, "_process_thread_name", "main", |
||||
"_service_version", "1.0.0", "_log_logger", "org.example.Test", "short_message", "message")); |
||||
assertThat(output).contains("'id' is an illegal field name according to GELF standard"); |
||||
} |
||||
|
||||
@Test |
||||
void shouldNotAddDoubleUnderscoreToCustomFields() { |
||||
MutableLogEvent event = createEvent(); |
||||
event.setContextData(new JdkMapAdapterStringMap(Map.of("_custom", "value"), true)); |
||||
String json = this.formatter.format(event); |
||||
assertThat(json).endsWith("\n"); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf( |
||||
map("version", "1.1", "host", "name", "timestamp", 1719910193.0, "level", 6, "_level_name", "INFO", |
||||
"_process_pid", 1, "_process_thread_name", "main", "_service_version", "1.0.0", "_log_logger", |
||||
"org.example.Test", "short_message", "message", "_custom", "value")); |
||||
} |
||||
|
||||
@Test |
||||
void shouldFormatException() { |
||||
MutableLogEvent event = createEvent(); |
||||
event.setThrown(new RuntimeException("Boom")); |
||||
String json = this.formatter.format(event); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
String fullMessage = (String) deserialized.get("full_message"); |
||||
String stackTrace = (String) deserialized.get("_error_stack_trace"); |
||||
assertThat(fullMessage).startsWith( |
||||
""" |
||||
message |
||||
|
||||
java.lang.RuntimeException: Boom |
||||
\tat org.springframework.boot.logging.log4j2.GraylogExtendedLogFormatStructuredLogFormatterTests.shouldFormatException"""); |
||||
assertThat(stackTrace).startsWith( |
||||
""" |
||||
java.lang.RuntimeException: Boom |
||||
\tat org.springframework.boot.logging.log4j2.GraylogExtendedLogFormatStructuredLogFormatterTests.shouldFormatException"""); |
||||
|
||||
assertThat(deserialized) |
||||
.containsAllEntriesOf(map("_error_type", "java.lang.RuntimeException", "_error_message", "Boom")); |
||||
assertThat(json).contains( |
||||
""" |
||||
message\\n\\njava.lang.RuntimeException: Boom\\n\\tat org.springframework.boot.logging.log4j2.GraylogExtendedLogFormatStructuredLogFormatterTests.shouldFormatException"""); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,148 @@
@@ -0,0 +1,148 @@
|
||||
/* |
||||
* 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://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.springframework.boot.logging.logback; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
|
||||
import ch.qos.logback.classic.spi.LoggingEvent; |
||||
import ch.qos.logback.classic.spi.ThrowableProxy; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.junit.jupiter.api.extension.ExtendWith; |
||||
|
||||
import org.springframework.boot.testsupport.system.CapturedOutput; |
||||
import org.springframework.boot.testsupport.system.OutputCaptureExtension; |
||||
import org.springframework.mock.env.MockEnvironment; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link GraylogExtendedLogFormatStructuredLogFormatter}. |
||||
* |
||||
* @author Samuel Lissner |
||||
* @author Moritz Halbritter |
||||
*/ |
||||
@ExtendWith(OutputCaptureExtension.class) |
||||
class GraylogExtendedLogFormatStructuredLogFormatterTests extends AbstractStructuredLoggingTests { |
||||
|
||||
private GraylogExtendedLogFormatStructuredLogFormatter formatter; |
||||
|
||||
@Override |
||||
@BeforeEach |
||||
void setUp() { |
||||
super.setUp(); |
||||
MockEnvironment environment = new MockEnvironment(); |
||||
environment.setProperty("logging.structured.gelf.service.name", "name"); |
||||
environment.setProperty("logging.structured.gelf.service.version", "1.0.0"); |
||||
environment.setProperty("spring.application.pid", "1"); |
||||
this.formatter = new GraylogExtendedLogFormatStructuredLogFormatter(environment, getThrowableProxyConverter()); |
||||
} |
||||
|
||||
@Test |
||||
void shouldFormat() { |
||||
LoggingEvent event = createEvent(); |
||||
event.setMDCPropertyMap(Map.of("mdc-1", "mdc-v-1")); |
||||
event.setKeyValuePairs(keyValuePairs("kv-1", "kv-v-1")); |
||||
String json = this.formatter.format(event); |
||||
assertThat(json).endsWith("\n"); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf( |
||||
map("version", "1.1", "host", "name", "timestamp", 1719910193.0, "level", 6, "_level_name", "INFO", |
||||
"_process_pid", 1, "_process_thread_name", "main", "_service_version", "1.0.0", "_log_logger", |
||||
"org.example.Test", "short_message", "message", "_mdc-1", "mdc-v-1", "_kv-1", "kv-v-1")); |
||||
} |
||||
|
||||
@Test |
||||
void shouldFormatMillisecondsInTimestamp() { |
||||
LoggingEvent event = createEvent(); |
||||
event.setTimeStamp(1719910193123L); |
||||
event.setMDCPropertyMap(Collections.emptyMap()); |
||||
String json = this.formatter.format(event); |
||||
assertThat(json).contains("\"timestamp\":1719910193.123"); |
||||
assertThat(json).endsWith("\n"); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("version", "1.1", "host", "name", "timestamp", |
||||
1719910193.123, "level", 6, "_level_name", "INFO", "_process_pid", 1, "_process_thread_name", "main", |
||||
"_service_version", "1.0.0", "_log_logger", "org.example.Test", "short_message", "message")); |
||||
} |
||||
|
||||
@Test |
||||
void shouldNotAllowInvalidFieldNames(CapturedOutput output) { |
||||
LoggingEvent event = createEvent(); |
||||
event.setMDCPropertyMap(Map.of("/", "value")); |
||||
String json = this.formatter.format(event); |
||||
assertThat(json).endsWith("\n"); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("version", "1.1", "host", "name", "timestamp", |
||||
1719910193.0, "level", 6, "_level_name", "INFO", "_process_pid", 1, "_process_thread_name", "main", |
||||
"_service_version", "1.0.0", "_log_logger", "org.example.Test", "short_message", "message")); |
||||
assertThat(output).contains("'/' is not a valid field name according to GELF standard"); |
||||
} |
||||
|
||||
@Test |
||||
void shouldNotAllowIllegalFieldNames(CapturedOutput output) { |
||||
LoggingEvent event = createEvent(); |
||||
event.setMDCPropertyMap(Map.of("id", "1")); |
||||
String json = this.formatter.format(event); |
||||
assertThat(json).endsWith("\n"); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("version", "1.1", "host", "name", "timestamp", |
||||
1719910193.0, "level", 6, "_level_name", "INFO", "_process_pid", 1, "_process_thread_name", "main", |
||||
"_service_version", "1.0.0", "_log_logger", "org.example.Test", "short_message", "message")); |
||||
assertThat(output).contains("'id' is an illegal field name according to GELF standard"); |
||||
} |
||||
|
||||
@Test |
||||
void shouldNotAddDoubleUnderscoreToCustomFields() { |
||||
LoggingEvent event = createEvent(); |
||||
event.setMDCPropertyMap(Map.of("_custom", "value")); |
||||
String json = this.formatter.format(event); |
||||
assertThat(json).endsWith("\n"); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf( |
||||
map("version", "1.1", "host", "name", "timestamp", 1719910193.0, "level", 6, "_level_name", "INFO", |
||||
"_process_pid", 1, "_process_thread_name", "main", "_service_version", "1.0.0", "_log_logger", |
||||
"org.example.Test", "short_message", "message", "_custom", "value")); |
||||
} |
||||
|
||||
@Test |
||||
void shouldFormatException() { |
||||
LoggingEvent event = createEvent(); |
||||
event.setMDCPropertyMap(Collections.emptyMap()); |
||||
event.setThrowableProxy(new ThrowableProxy(new RuntimeException("Boom"))); |
||||
String json = this.formatter.format(event); |
||||
Map<String, Object> deserialized = deserialize(json); |
||||
String fullMessage = (String) deserialized.get("full_message"); |
||||
String stackTrace = (String) deserialized.get("_error_stack_trace"); |
||||
assertThat(fullMessage).startsWith( |
||||
"message\n\njava.lang.RuntimeException: Boom%n\tat org.springframework.boot.logging.logback.GraylogExtendedLogFormatStructuredLogFormatterTests.shouldFormatException" |
||||
.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()); |
||||
assertThat(json).contains( |
||||
"java.lang.RuntimeException: Boom%n\\tat org.springframework.boot.logging.logback.GraylogExtendedLogFormatStructuredLogFormatterTests.shouldFormatException" |
||||
.formatted() |
||||
.replace("\n", "\\n") |
||||
.replace("\r", "\\r")); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
/* |
||||
* 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://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.springframework.boot.logging.structured; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.boot.json.JsonWriter; |
||||
import org.springframework.mock.env.MockEnvironment; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link GraylogExtendedLogFormatService}. |
||||
* |
||||
* @author Samuel Lissner |
||||
*/ |
||||
class GraylogExtendedLogFormatServiceTests { |
||||
|
||||
@Test |
||||
void getBindsFromEnvironment() { |
||||
MockEnvironment environment = new MockEnvironment(); |
||||
environment.setProperty("logging.structured.gelf.service.name", "spring"); |
||||
environment.setProperty("logging.structured.gelf.service.version", "1.2.3"); |
||||
GraylogExtendedLogFormatService service = GraylogExtendedLogFormatService.get(environment); |
||||
assertThat(service).isEqualTo(new GraylogExtendedLogFormatService("spring", "1.2.3")); |
||||
} |
||||
|
||||
@Test |
||||
void getWhenNoServiceNameUsesApplicationName() { |
||||
MockEnvironment environment = new MockEnvironment(); |
||||
environment.setProperty("spring.application.name", "spring"); |
||||
GraylogExtendedLogFormatService service = GraylogExtendedLogFormatService.get(environment); |
||||
assertThat(service).isEqualTo(new GraylogExtendedLogFormatService("spring", null)); |
||||
} |
||||
|
||||
@Test |
||||
void getWhenNoServiceVersionUsesApplicationVersion() { |
||||
MockEnvironment environment = new MockEnvironment(); |
||||
environment.setProperty("spring.application.version", "1.2.3"); |
||||
GraylogExtendedLogFormatService service = GraylogExtendedLogFormatService.get(environment); |
||||
assertThat(service).isEqualTo(new GraylogExtendedLogFormatService(null, "1.2.3")); |
||||
} |
||||
|
||||
@Test |
||||
void getWhenNoPropertiesToBind() { |
||||
MockEnvironment environment = new MockEnvironment(); |
||||
GraylogExtendedLogFormatService service = GraylogExtendedLogFormatService.get(environment); |
||||
assertThat(service).isEqualTo(new GraylogExtendedLogFormatService(null, null)); |
||||
} |
||||
|
||||
@Test |
||||
void addToJsonMembersCreatesValidJson() { |
||||
GraylogExtendedLogFormatService service = new GraylogExtendedLogFormatService("spring", "1.2.3"); |
||||
JsonWriter<GraylogExtendedLogFormatService> writer = JsonWriter.of(service::jsonMembers); |
||||
assertThat(writer.writeToString(service)).isEqualTo("{\"host\":\"spring\",\"_service_version\":\"1.2.3\"}"); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue