This is an automated email from the ASF dual-hosted git repository. vy pushed a commit to branch LOG4J2-3116 in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 0448aa9be4081141c08ce14cbe3d7fd9262f3da0 Author: Volkan Yazici <[email protected]> AuthorDate: Fri Jul 9 13:47:11 2021 +0200 LOG4J2-3116 Add GCP logging layout. --- .../src/main/resources/GcpLayout.json | 67 +++++++ .../log4j/layout/template/json/GcpLayoutTest.java | 221 +++++++++++++++++++++ src/changes/changes.xml | 3 + .../asciidoc/manual/json-template-layout.adoc.vm | 9 + 4 files changed, 300 insertions(+) diff --git a/log4j-layout-template-json/src/main/resources/GcpLayout.json b/log4j-layout-template-json/src/main/resources/GcpLayout.json new file mode 100644 index 0000000..cdc1f56 --- /dev/null +++ b/log4j-layout-template-json/src/main/resources/GcpLayout.json @@ -0,0 +1,67 @@ +{ + "timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "timeZone": "UTC", + "locale": "en_US" + } + }, + "severity": { + "$resolver": "pattern", + "pattern": "%level{WARN=WARNING, TRACE=DEBUG, FATAL=EMERGENCY}", + "stackTraceEnabled": false + }, + "message": { + "$resolver": "pattern", + "pattern": "%m" + }, + "logging.googleapis.com/labels": { + "$resolver": "mdc", + "stringified": true + }, + "logging.googleapis.com/sourceLocation": { + "file": { + "$resolver": "source", + "field": "fileName" + }, + "line": { + "$resolver": "source", + "field": "lineNumber" + }, + "function": { + "$resolver": "pattern", + "pattern": "%replace{%C.%M}{^\\?\\.$}{}", + "stackTraceEnabled": false + } + }, + "logging.googleapis.com/insertId": { + "$resolver": "counter", + "stringified": true + }, + "_exception": { + "class": { + "$resolver": "exception", + "field": "className" + }, + "message": { + "$resolver": "exception", + "field": "message" + }, + "stackTrace": { + "$resolver": "exception", + "field": "stackTrace", + "stackTrace": { + "stringified": true + } + } + }, + "_thread": { + "$resolver": "thread", + "field": "name" + }, + "_logger": { + "$resolver": "logger", + "field": "name" + } +} diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/GcpLayoutTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/GcpLayoutTest.java new file mode 100644 index 0000000..7ed69f1 --- /dev/null +++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/GcpLayoutTest.java @@ -0,0 +1,221 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.logging.log4j.layout.template.json; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.LogEvent; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.stream.Stream; + +import static org.apache.logging.log4j.layout.template.json.TestHelpers.CONFIGURATION; +import static org.apache.logging.log4j.layout.template.json.TestHelpers.usingSerializedLogEventAccessor; +import static org.assertj.core.api.Assertions.assertThat; + +class GcpLayoutTest { + + private static final JsonTemplateLayout LAYOUT = JsonTemplateLayout + .newBuilder() + .setConfiguration(CONFIGURATION) + .setStackTraceEnabled(true) + .setLocationInfoEnabled(true) + .setEventTemplateUri("classpath:GcpLayout.json") + .build(); + + private static final int LOG_EVENT_COUNT = 1_000; + + private static final DateTimeFormatter DATE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + + @ParameterizedTest + @MethodSource("createLiteLogEvents") + void test_lite_log_events(final LogEvent logEvent) { + verifySerialization(logEvent); + } + + @SuppressWarnings("unused") // supplies arguments to test_lite_log_events() + private static Stream<Arguments> createLiteLogEvents() { + return LogEventFixture + .createLiteLogEvents(LOG_EVENT_COUNT) + .stream() + .map(Arguments::arguments); + } + + @ParameterizedTest + @MethodSource("createFullLogEvents") + void test_full_log_events(final LogEvent logEvent) { + verifySerialization(logEvent); + } + + @SuppressWarnings("unused") // supplies arguments to test_full_log_events() + private static Stream<Arguments> createFullLogEvents() { + return LogEventFixture + .createFullLogEvents(LOG_EVENT_COUNT) + .stream() + .map(Arguments::arguments); + } + + void verifySerialization(final LogEvent logEvent) { + usingSerializedLogEventAccessor(LAYOUT, logEvent, accessor -> { + + // Verify timestamp. + final String expectedTimestamp = formatLogEventInstant(logEvent); + assertThat(accessor.getString("timestamp")).isEqualTo(expectedTimestamp); + + // Verify severity. + final Level level = logEvent.getLevel(); + final String expectedSeverity; + if (Level.WARN.equals(level)) { + expectedSeverity = "WARNING"; + } else if (Level.TRACE.equals(level)) { + expectedSeverity = "TRACE"; + } else if (Level.FATAL.equals(level)) { + expectedSeverity = "EMERGENCY"; + } else { + expectedSeverity = level.name(); + } + assertThat(accessor.getString("severity")).isEqualTo(expectedSeverity); + + // Verify message. + final String expectedMessage = logEvent.getMessage().getFormattedMessage(); + assertThat(accessor.getString("message")).contains(expectedMessage); + final Throwable exception = logEvent.getThrown(); + if (exception != null) { + final String expectedExceptionMessage = exception.getLocalizedMessage(); + assertThat(accessor.getString("message")).contains(expectedExceptionMessage); + } + + // Verify labels. + logEvent.getContextData().forEach((key, value) -> { + final String expectedValue = String.valueOf(value); + final String actualValue = + accessor.getString(new String[]{ + "logging.googleapis.com/labels", key}); + assertThat(actualValue).isEqualTo(expectedValue); + }); + + final StackTraceElement source = logEvent.getSource(); + if (source != null) { + + // Verify file name. + final String actualFileName = + accessor.getString(new String[]{ + "logging.googleapis.com/sourceLocation", "file"}); + assertThat(actualFileName).isEqualTo(source.getFileName()); + + // Verify line number. + final int actualLineNumber = + accessor.getInteger(new String[]{ + "logging.googleapis.com/sourceLocation", "line"}); + assertThat(actualLineNumber).isEqualTo(source.getLineNumber()); + + // Verify function. + final String expectedFunction = + source.getClassName() + "." + source.getMethodName(); + final String actualFunction = + accessor.getString(new String[]{ + "logging.googleapis.com/sourceLocation", "function"}); + assertThat(actualFunction).isEqualTo(expectedFunction); + + } else { + assertThat(accessor.exists( + new String[]{"logging.googleapis.com/sourceLocation", "file"})) + .isFalse(); + assertThat(accessor.exists( + new String[]{"logging.googleapis.com/sourceLocation", "line"})) + .isFalse(); + assertThat(accessor.getString( + new String[]{"logging.googleapis.com/sourceLocation", "function"})) + .isEmpty(); + } + + // Verify insert id. + assertThat(accessor.getString("logging.googleapis.com/insertId")) + .matches("[-]?[0-9]+"); + + // Verify exception. + if (exception != null) { + + // Verify exception class. + assertThat(accessor.getString( + new String[]{"_exception", "class"})) + .isEqualTo(exception.getClass().getCanonicalName()); + + // Verify exception message. + assertThat(accessor.getString( + new String[]{"_exception", "message"})) + .isEqualTo(exception.getMessage()); + + // Verify exception stack trace. + final String expectedExceptionStackTrace = + serializeThrowableStackTrace(exception); + assertThat(accessor.getString( + new String[]{"_exception", "stackTrace"})) + .isEqualTo(expectedExceptionStackTrace); + + } else { + assertThat(accessor.getObject( + new String[]{"_exception", "class"})) + .isNull(); + assertThat(accessor.getObject( + new String[]{"_exception", "message"})) + .isNull(); + assertThat(accessor.getObject( + new String[]{"_exception", "stackTrace"})) + .isNull(); + } + + // Verify thread name. + assertThat(accessor.getString("_thread")) + .isEqualTo(logEvent.getThreadName()); + + // Verify logger name. + assertThat(accessor.getString("_logger")) + .isEqualTo(logEvent.getLoggerName()); + + }); + } + + private static String formatLogEventInstant(final LogEvent logEvent) { + org.apache.logging.log4j.core.time.Instant instant = logEvent.getInstant(); + ZonedDateTime dateTime = Instant.ofEpochSecond( + instant.getEpochSecond(), + instant.getNanoOfSecond()).atZone(ZoneId.of("UTC")); + return DATE_TIME_FORMATTER.format(dateTime); + } + + private static String serializeThrowableStackTrace(final Throwable throwable) { + try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + final PrintWriter writer = new PrintWriter(outputStream)) { + throwable.printStackTrace(writer); + writer.flush(); + return outputStream.toString(LAYOUT.getCharset().name()); + } catch (final Exception error) { + throw new RuntimeException(error); + } + } + +} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 4751456..a42ae82 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -31,6 +31,9 @@ --> <release version="2.15.0" date="2021-MM-DD" description="GA Release 2.15.0"> <!-- ADDS --> + <action issue="LOG4J2-3116" dev="rgupta"> + Add GCP logging layout. + </action> <action issue="LOG4J2-3067" dev="vy" type="add"> Add CounterResolver to JsonTemplateLayout. </action> diff --git a/src/site/asciidoc/manual/json-template-layout.adoc.vm b/src/site/asciidoc/manual/json-template-layout.adoc.vm index 58754c7..53410b2 100644 --- a/src/site/asciidoc/manual/json-template-layout.adoc.vm +++ b/src/site/asciidoc/manual/json-template-layout.adoc.vm @@ -410,6 +410,15 @@ artifact, which contains the following predefined event templates: xref:additional-event-template-fields[additional event template fields] to avoid `hostName` property lookup at runtime, which incurs an extra cost.) +- https://github.com/apache/logging-log4j2/tree/master/log4j-layout-template-json/src/main/resources/GcpLayout.json[`GcpLayout.json`] + described by https://cloud.google.com/logging/docs/structured-logging[Google + Cloud Platform structured logging] with additional + `_thread`, `_logger` and `_exception` fields. The exception trace, if any, + is written to the `_exception` field as well as the `message` field – + the former is useful for explicitly searching/analyzing structured exception + information, while the latter is Google's expected place for the exception, + and integrates with https://cloud.google.com/error-reporting[Google Error Reporting]. + - https://github.com/apache/logging-log4j2/tree/master/log4j-layout-template-json/src/main/resources/JsonLayout.json[`JsonLayout.json`] providing the exact JSON structure generated by link:layouts.html#JSONLayout[`JsonLayout`] with the exception of `thrown` field. (`JsonLayout` serializes the `Throwable`
