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:30 UTC

[logging-log4j2] branch LOG4J2-3116 created (now 0448aa9)

This is an automated email from the ASF dual-hosted git repository.

vy pushed a change to branch LOG4J2-3116
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git.


      at 0448aa9  LOG4J2-3116 Add GCP logging layout.

This branch includes the following new commits:

     new 0448aa9  LOG4J2-3116 Add GCP logging layout.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[logging-log4j2] 01/01: LOG4J2-3116 Add GCP logging layout.

Posted by vy...@apache.org.
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`