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`