You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2021/07/19 21:29:31 UTC
[logging-log4j2] 01/01: LOG4J2-3116 Add GCP logging layout.
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 <vo...@gmail.com>
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`