From 9c3b46c2e8fb9d1a486b3650cd51e00308f97d06 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Fri, 30 Oct 2020 18:17:38 +0100 Subject: [PATCH 1/4] Support for log4j 2.8 This is the earliest version to support builders --- log4j2-ecs-layout/README.md | 2 ++ .../co/elastic/logging/log4j2/EcsLayout.java | 24 ++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/log4j2-ecs-layout/README.md b/log4j2-ecs-layout/README.md index 5072624a..c58e2126 100644 --- a/log4j2-ecs-layout/README.md +++ b/log4j2-ecs-layout/README.md @@ -1,5 +1,7 @@ # Log4j2 ECS Layout +The minimum required log4j2 version is 2.8. + ## Step 1: add dependency Latest version: [![Maven Central](https://img.shields.io/maven-central/v/co.elastic.logging/log4j2-ecs-layout.svg)](https://search.maven.org/search?q=g:co.elastic.logging%20AND%20a:log4j2-ecs-layout) diff --git a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java index 1e7827d1..709b4343 100644 --- a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java +++ b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java @@ -46,10 +46,10 @@ import org.apache.logging.log4j.message.Message; import org.apache.logging.log4j.message.MultiformatMessage; import org.apache.logging.log4j.message.ObjectMessage; -import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable; import org.apache.logging.log4j.util.StringBuilderFormattable; import org.apache.logging.log4j.util.TriConsumer; +import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -59,8 +59,23 @@ public class EcsLayout extends AbstractStringLayout { public static final Charset UTF_8 = Charset.forName("UTF-8"); + public static final Class MULTI_FORMAT_STRING_BUILDER_FORMATTABLE; + public static final Method FORMAT_TO; static final String[] JSON_FORMAT = {"JSON"}; + static { + Method formatTo = null; + Class clazz = null; + try { + clazz = Class.forName("org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable"); + formatTo = clazz + .getMethod("formatTo", String[].class, StringBuilder.class); + } catch (ReflectiveOperationException ignore) { + } + FORMAT_TO = formatTo; + MULTI_FORMAT_STRING_BUILDER_FORMATTABLE = clazz; + } + private final TriConsumer WRITE_MDC = new TriConsumer() { @Override public void accept(final String key, final Object value, final StringBuilder stringBuilder) { @@ -246,8 +261,11 @@ private void serializeMessage(StringBuilder builder, boolean gcFree, Message mes private static void serializeJsonMessage(StringBuilder builder, MultiformatMessage message) { final StringBuilder messageBuffer = EcsJsonSerializer.getMessageStringBuilder(); - if (message instanceof MultiFormatStringBuilderFormattable) { - ((MultiFormatStringBuilderFormattable) message).formatTo(JSON_FORMAT, messageBuffer); + if (MULTI_FORMAT_STRING_BUILDER_FORMATTABLE.isInstance(message)) { + try { + FORMAT_TO.invoke(message, JSON_FORMAT, messageBuffer); + } catch (ReflectiveOperationException ignore) { + } } else { messageBuffer.append(message.getFormattedMessage(JSON_FORMAT)); } From e1247d5f901ca6e7ca7d68b7e2dd701ac50460bc Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Fri, 30 Oct 2020 18:31:54 +0100 Subject: [PATCH 2/4] Avoid using 1.7 introduced ReflectiveOperationException --- .../java/co/elastic/logging/log4j2/EcsLayout.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java index 709b4343..472dd5f1 100644 --- a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java +++ b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java @@ -46,9 +46,11 @@ import org.apache.logging.log4j.message.Message; import org.apache.logging.log4j.message.MultiformatMessage; import org.apache.logging.log4j.message.ObjectMessage; +import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.StringBuilderFormattable; import org.apache.logging.log4j.util.TriConsumer; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.List; @@ -70,7 +72,7 @@ public class EcsLayout extends AbstractStringLayout { clazz = Class.forName("org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable"); formatTo = clazz .getMethod("formatTo", String[].class, StringBuilder.class); - } catch (ReflectiveOperationException ignore) { + } catch (Exception ignore) { } FORMAT_TO = formatTo; MULTI_FORMAT_STRING_BUILDER_FORMATTABLE = clazz; @@ -264,7 +266,15 @@ private static void serializeJsonMessage(StringBuilder builder, MultiformatMessa if (MULTI_FORMAT_STRING_BUILDER_FORMATTABLE.isInstance(message)) { try { FORMAT_TO.invoke(message, JSON_FORMAT, messageBuffer); - } catch (ReflectiveOperationException ignore) { + } catch (IllegalAccessException e) { + StatusLogger.getLogger().error(e); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + StatusLogger.getLogger().error(e); + } } } else { messageBuffer.append(message.getFormattedMessage(JSON_FORMAT)); From 3d1128f995127008e78d5e2d572f41d384ef0fbc Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Wed, 11 Nov 2020 16:14:38 +0100 Subject: [PATCH 3/4] Drop min version down to 2.7 --- log4j2-ecs-layout/README.md | 2 +- .../co/elastic/logging/log4j2/EcsLayout.java | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/log4j2-ecs-layout/README.md b/log4j2-ecs-layout/README.md index c58e2126..fdb70b8f 100644 --- a/log4j2-ecs-layout/README.md +++ b/log4j2-ecs-layout/README.md @@ -1,6 +1,6 @@ # Log4j2 ECS Layout -The minimum required log4j2 version is 2.8. +The minimum required log4j2 version is 2.7. ## Step 1: add dependency diff --git a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java index 472dd5f1..ba8f08e9 100644 --- a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java +++ b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java @@ -35,6 +35,7 @@ import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.layout.AbstractStringLayout; import org.apache.logging.log4j.core.layout.ByteBufferDestination; @@ -120,7 +121,7 @@ private EcsLayout(Configuration config, String serviceName, String eventDataset, @PluginBuilderFactory public static EcsLayout.Builder newBuilder() { - return new EcsLayout.Builder().asBuilder(); + return new EcsLayout.Builder(); } private static boolean valueNeedsLookup(final String value) { @@ -347,9 +348,10 @@ private boolean supportsJson(MultiformatMessage message) { return supportsJson; } - public static class Builder extends AbstractStringLayout.Builder - implements org.apache.logging.log4j.core.util.Builder { + public static class Builder implements org.apache.logging.log4j.core.util.Builder { + @PluginConfiguration + private Configuration configuration; @PluginBuilderAttribute("serviceName") private String serviceName; @PluginBuilderAttribute("eventDataset") @@ -364,8 +366,15 @@ public static class Builder extends AbstractStringLayout.Builder Date: Fri, 13 Nov 2020 15:54:29 +0100 Subject: [PATCH 4/4] Drop min version down to 2.6 --- log4j2-ecs-layout/pom.xml | 16 ++- .../co/elastic/logging/log4j2/EcsLayout.java | 135 +++++++----------- .../elastic/logging/log4j2/MdcSerializer.java | 86 +++++++++++ .../logging/log4j2/MultiFormatHandler.java | 79 ++++++++++ .../ObjectMessageJacksonSerializer.java | 12 +- .../log4j2/AbstractLog4j2EcsLayoutTest.java | 66 +-------- .../log4j2/CurrentLog4j2EcsLayoutTest.java | 97 +++++++++++++ .../logging/log4j2/Log4j2EcsLayoutTest.java | 4 +- log4j2-legacy-tests/pom.xml | 62 ++++++++ .../log4j2/LegacyLog4j2EcsLayoutTest.java | 30 ++++ pom.xml | 9 +- 11 files changed, 437 insertions(+), 159 deletions(-) create mode 100644 log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/MdcSerializer.java create mode 100644 log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/MultiFormatHandler.java create mode 100644 log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/CurrentLog4j2EcsLayoutTest.java create mode 100644 log4j2-legacy-tests/pom.xml create mode 100644 log4j2-legacy-tests/src/test/java/co/elastic/logging/log4j2/LegacyLog4j2EcsLayoutTest.java diff --git a/log4j2-ecs-layout/pom.xml b/log4j2-ecs-layout/pom.xml index 12bf168e..38479129 100644 --- a/log4j2-ecs-layout/pom.xml +++ b/log4j2-ecs-layout/pom.xml @@ -9,6 +9,7 @@ ${project.basedir}/.. + 2.14.0 log4j2-ecs-layout @@ -30,12 +31,25 @@ only - org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor + org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + + test-jar + + + + diff --git a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java index ba8f08e9..e4eb7e9e 100644 --- a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java +++ b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java @@ -28,6 +28,7 @@ import co.elastic.logging.EcsJsonSerializer; import co.elastic.logging.JsonUtils; import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.ThreadContext; import org.apache.logging.log4j.core.Layout; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.config.Configuration; @@ -47,13 +48,10 @@ import org.apache.logging.log4j.message.Message; import org.apache.logging.log4j.message.MultiformatMessage; import org.apache.logging.log4j.message.ObjectMessage; -import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.StringBuilderFormattable; -import org.apache.logging.log4j.util.TriConsumer; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.nio.charset.Charset; +import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -62,33 +60,9 @@ public class EcsLayout extends AbstractStringLayout { public static final Charset UTF_8 = Charset.forName("UTF-8"); - public static final Class MULTI_FORMAT_STRING_BUILDER_FORMATTABLE; - public static final Method FORMAT_TO; - static final String[] JSON_FORMAT = {"JSON"}; - - static { - Method formatTo = null; - Class clazz = null; - try { - clazz = Class.forName("org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable"); - formatTo = clazz - .getMethod("formatTo", String[].class, StringBuilder.class); - } catch (Exception ignore) { - } - FORMAT_TO = formatTo; - MULTI_FORMAT_STRING_BUILDER_FORMATTABLE = clazz; - } - - private final TriConsumer WRITE_MDC = new TriConsumer() { - @Override - public void accept(final String key, final Object value, final StringBuilder stringBuilder) { - stringBuilder.append('\"'); - JsonUtils.quoteAsString(key, stringBuilder); - stringBuilder.append("\":\""); - JsonUtils.quoteAsString(EcsJsonSerializer.toNullSafeString(String.valueOf(value)), stringBuilder); - stringBuilder.append("\","); - } - }; + private static final ObjectMessageJacksonSerializer JACKSON_SERIALIZER = ObjectMessageJacksonSerializer.Resolver.resolve(); + private static final MdcSerializer MDC_SERIALIZER = MdcSerializer.Resolver.resolve(); + private static final MultiFormatHandler MULTI_FORMAT_HANDLER = MultiFormatHandler.Resolver.resolve(); private final KeyValuePair[] additionalFields; private final PatternFormatter[][] fieldValuePatternFormatter; @@ -98,7 +72,6 @@ public void accept(final String key, final Object value, final StringBuilder str private final boolean includeMarkers; private final boolean includeOrigin; private final ConcurrentMap, Boolean> supportsJson = new ConcurrentHashMap, Boolean>(); - private final ObjectMessageJacksonSerializer objectMessageJacksonSerializer = ObjectMessageJacksonSerializer.Resolver.INSTANCE.resolve(); private EcsLayout(Configuration config, String serviceName, String eventDataset, boolean includeMarkers, KeyValuePair[] additionalFields, boolean includeOrigin, boolean stackTraceAsArray) { super(config, UTF_8, null, null); @@ -166,40 +139,38 @@ private StringBuilder toText(LogEvent event, StringBuilder builder, boolean gcFr private void serializeAdditionalFieldsAndMDC(LogEvent event, StringBuilder builder) { final int length = additionalFields.length; - if (!event.getContextData().isEmpty() || length > 0) { - if (length > 0) { - final StrSubstitutor strSubstitutor = getConfiguration().getStrSubstitutor(); - for (int i = 0; i < length; i++) { - KeyValuePair additionalField = additionalFields[i]; - PatternFormatter[] formatters = fieldValuePatternFormatter[i]; - CharSequence value = null; - if (formatters != null) { - StringBuilder buffer = EcsJsonSerializer.getMessageStringBuilder(); - formatPattern(event, formatters, buffer); - if (buffer.length() > 0) { - value = buffer; - } - } else if (valueNeedsLookup(additionalField.getValue())) { - StringBuilder lookupValue = EcsJsonSerializer.getMessageStringBuilder(); - lookupValue.append(additionalField.getValue()); - if (strSubstitutor.replaceIn(event, lookupValue)) { - value = lookupValue; - } - } else { - value = additionalField.getValue(); + if (length > 0) { + final StrSubstitutor strSubstitutor = getConfiguration().getStrSubstitutor(); + for (int i = 0; i < length; i++) { + KeyValuePair additionalField = additionalFields[i]; + PatternFormatter[] formatters = fieldValuePatternFormatter[i]; + CharSequence value = null; + if (formatters != null) { + StringBuilder buffer = EcsJsonSerializer.getMessageStringBuilder(); + formatPattern(event, formatters, buffer); + if (buffer.length() > 0) { + value = buffer; } - - if (value != null) { - builder.append('\"'); - JsonUtils.quoteAsString(additionalField.getKey(), builder); - builder.append("\":\""); - JsonUtils.quoteAsString(EcsJsonSerializer.toNullSafeString(value), builder); - builder.append("\","); + } else if (valueNeedsLookup(additionalField.getValue())) { + StringBuilder lookupValue = EcsJsonSerializer.getMessageStringBuilder(); + lookupValue.append(additionalField.getValue()); + if (strSubstitutor.replaceIn(event, lookupValue)) { + value = lookupValue; } + } else { + value = additionalField.getValue(); + } + + if (value != null) { + builder.append('\"'); + JsonUtils.quoteAsString(additionalField.getKey(), builder); + builder.append("\":\""); + JsonUtils.quoteAsString(EcsJsonSerializer.toNullSafeString(value), builder); + builder.append("\","); } } - event.getContextData().forEach(WRITE_MDC, builder); } + MDC_SERIALIZER.serializeMdc(event, builder); } private static void formatPattern(LogEvent event, PatternFormatter[] formatters, StringBuilder buffer) { @@ -210,7 +181,13 @@ private static void formatPattern(LogEvent event, PatternFormatter[] formatters, } private void serializeTags(LogEvent event, StringBuilder builder) { - List contextStack = event.getContextStack().asList(); + ThreadContext.ContextStack stack = event.getContextStack(); + List contextStack; + if (stack == null) { + contextStack = Collections.emptyList(); + } else { + contextStack = stack.asList(); + } Marker marker = event.getMarker(); boolean hasTags = !contextStack.isEmpty() || (includeMarkers && marker != null); if (hasTags) { @@ -253,9 +230,9 @@ private void serializeMessage(StringBuilder builder, boolean gcFree, Message mes } else { serializeSimpleMessage(builder, gcFree, message, thrown); } - } else if (objectMessageJacksonSerializer != null && message instanceof ObjectMessage) { + } else if (JACKSON_SERIALIZER != null && message instanceof ObjectMessage) { final StringBuilder jsonBuffer = EcsJsonSerializer.getMessageStringBuilder(); - objectMessageJacksonSerializer.formatTo(jsonBuffer, (ObjectMessage) message); + JACKSON_SERIALIZER.formatTo(jsonBuffer, (ObjectMessage) message); addJson(builder, jsonBuffer); } else { serializeSimpleMessage(builder, gcFree, message, thrown); @@ -264,22 +241,7 @@ private void serializeMessage(StringBuilder builder, boolean gcFree, Message mes private static void serializeJsonMessage(StringBuilder builder, MultiformatMessage message) { final StringBuilder messageBuffer = EcsJsonSerializer.getMessageStringBuilder(); - if (MULTI_FORMAT_STRING_BUILDER_FORMATTABLE.isInstance(message)) { - try { - FORMAT_TO.invoke(message, JSON_FORMAT, messageBuffer); - } catch (IllegalAccessException e) { - StatusLogger.getLogger().error(e); - } catch (InvocationTargetException e) { - Throwable cause = e.getCause(); - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } else { - StatusLogger.getLogger().error(e); - } - } - } else { - messageBuffer.append(message.getFormattedMessage(JSON_FORMAT)); - } + MULTI_FORMAT_HANDLER.formatJsonTo(message, messageBuffer); addJson(builder, messageBuffer); } @@ -312,7 +274,7 @@ private void serializeSimpleMessage(StringBuilder builder, boolean gcFree, Messa ((StringBuilderFormattable) message).formatTo(messageBuffer); JsonUtils.quoteAsString(messageBuffer, builder); } finally { - trimToMaxSize(messageBuffer); + trimToMaxSizeCopy(messageBuffer); } } else { JsonUtils.quoteAsString(EcsJsonSerializer.toNullSafeString(message.getFormattedMessage()), builder); @@ -320,12 +282,19 @@ private void serializeSimpleMessage(StringBuilder builder, boolean gcFree, Messa builder.append("\", "); } + static void trimToMaxSizeCopy(final StringBuilder stringBuilder) { + if (stringBuilder.length() > MAX_STRING_BUILDER_SIZE) { + stringBuilder.setLength(MAX_STRING_BUILDER_SIZE); + stringBuilder.trimToSize(); + } + } + private static boolean isObject(StringBuilder messageBuffer) { - return messageBuffer.length() > 1 && messageBuffer.charAt(0) == '{' && messageBuffer.charAt(messageBuffer.length() -1) == '}'; + return messageBuffer.length() > 1 && messageBuffer.charAt(0) == '{' && messageBuffer.charAt(messageBuffer.length() - 1) == '}'; } private static boolean isString(StringBuilder messageBuffer) { - return messageBuffer.length() > 1 && messageBuffer.charAt(0) == '"' && messageBuffer.charAt(messageBuffer.length() -1) == '"'; + return messageBuffer.length() > 1 && messageBuffer.charAt(0) == '"' && messageBuffer.charAt(messageBuffer.length() - 1) == '"'; } private static void moveToRoot(StringBuilder messageBuffer) { diff --git a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/MdcSerializer.java b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/MdcSerializer.java new file mode 100644 index 00000000..fdd1fba4 --- /dev/null +++ b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/MdcSerializer.java @@ -0,0 +1,86 @@ +/*- + * #%L + * Java ECS logging + * %% + * Copyright (C) 2019 - 2020 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + * #L% + */ +package co.elastic.logging.log4j2; + +import co.elastic.logging.EcsJsonSerializer; +import co.elastic.logging.JsonUtils; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.util.TriConsumer; + +interface MdcSerializer { + + void serializeMdc(LogEvent event, StringBuilder builder); + + class Resolver { + + public static MdcSerializer resolve() { + try { + LogEvent.class.getMethod("getContextData"); + return (MdcSerializer) Class.forName("co.elastic.logging.log4j2.MdcSerializer$UsingContextData").getEnumConstants()[0]; + } catch (Exception ignore) { + } catch (LinkageError ignore) { + } + return UsingContextMap.INSTANCE; + } + + } + + /** + * Garbage free MDC serialization for log4j2 2.7+ + * Never reference directly in prod code so avoid linkage errors when TriConsumer or getContextData are not available + */ + enum UsingContextData implements MdcSerializer { + INSTANCE; + + private static final TriConsumer WRITE_MDC = new TriConsumer() { + @Override + public void accept(final String key, final Object value, final StringBuilder stringBuilder) { + stringBuilder.append('\"'); + JsonUtils.quoteAsString(key, stringBuilder); + stringBuilder.append("\":\""); + JsonUtils.quoteAsString(EcsJsonSerializer.toNullSafeString(String.valueOf(value)), stringBuilder); + stringBuilder.append("\","); + } + }; + + + @Override + public void serializeMdc(LogEvent event, StringBuilder builder) { + event.getContextData().forEach(WRITE_MDC, builder); + } + } + + /** + * Fallback for log4j2 <= 2.6 + */ + enum UsingContextMap implements MdcSerializer { + INSTANCE; + + @Override + public void serializeMdc(LogEvent event, StringBuilder builder) { + EcsJsonSerializer.serializeMDC(builder, event.getContextMap()); + } + } +} diff --git a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/MultiFormatHandler.java b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/MultiFormatHandler.java new file mode 100644 index 00000000..684badf0 --- /dev/null +++ b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/MultiFormatHandler.java @@ -0,0 +1,79 @@ +/*- + * #%L + * Java ECS logging + * %% + * Copyright (C) 2019 - 2020 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + * #L% + */ +package co.elastic.logging.log4j2; + +import org.apache.logging.log4j.message.MultiformatMessage; +import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable; + +interface MultiFormatHandler { + + void formatJsonTo(MultiformatMessage message, StringBuilder builder); + + class Resolver { + + static MultiFormatHandler resolve() { + try { + Class.forName("org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable"); + return (MultiFormatHandler) Class.forName("co.elastic.logging.log4j2.MultiFormatHandler$MultiFormatStringBuilderFormattableAware").getEnumConstants()[0]; + } catch (Exception ignore) { + } catch (LinkageError ignore) { + } + return ForLegacyLog4j.INSTANCE; + } + + } + + /** + * For log4j2 >= 2.10 + * Never reference directly in prod code so avoid linkage errors when {@link MultiFormatStringBuilderFormattable} is not available + */ + enum MultiFormatStringBuilderFormattableAware implements MultiFormatHandler { + INSTANCE; + private static final String[] JSON_FORMAT = {"JSON"}; + + @Override + public void formatJsonTo(MultiformatMessage message, StringBuilder builder) { + if (message instanceof MultiFormatStringBuilderFormattable) { + ((MultiFormatStringBuilderFormattable) message).formatTo(JSON_FORMAT, builder); + } else { + builder.append(message.getFormattedMessage(JSON_FORMAT)); + } + } + } + + /** + * Fallback for log4j2 < 2.10 + */ + enum ForLegacyLog4j implements MultiFormatHandler { + INSTANCE; + private static final String[] JSON_FORMAT = {"JSON"}; + + @Override + public void formatJsonTo(MultiformatMessage message, StringBuilder builder) { + builder.append(message.getFormattedMessage(JSON_FORMAT)); + } + } + +} diff --git a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/ObjectMessageJacksonSerializer.java b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/ObjectMessageJacksonSerializer.java index 20c9da76..c1c3b950 100644 --- a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/ObjectMessageJacksonSerializer.java +++ b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/ObjectMessageJacksonSerializer.java @@ -37,24 +37,22 @@ interface ObjectMessageJacksonSerializer { void formatTo(StringBuilder buffer, ObjectMessage objectMessage); - enum Resolver { - INSTANCE; + class Resolver { - ObjectMessageJacksonSerializer resolve() { - ObjectMessageJacksonSerializer localDelegate = null; + static ObjectMessageJacksonSerializer resolve() { try { // safely discovers if Jackson is available Class.forName("com.fasterxml.jackson.databind.ObjectMapper"); // this method has been introduced in 2.7 Class.forName("org.apache.logging.log4j.message.ObjectMessage").getMethod("getParameter"); // avoid initializing ObjectMessageSerializer$WithJackson if Jackson is not on the classpath to avoid linkage errors - return (ObjectMessageJacksonSerializer) Class.forName("co.elastic.logging.log4j2.ObjectMessageJacksonSerializer$Available").getEnumConstants()[0]; + return (ObjectMessageJacksonSerializer) Class.forName("co.elastic.logging.log4j2.ObjectMessageJacksonSerializer$Available").getEnumConstants()[0]; } catch (Exception e) { - return null; } catch (LinkageError e) { // we should not cause linkage errors but just in case... - return null; } + return null; + } } diff --git a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/AbstractLog4j2EcsLayoutTest.java b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/AbstractLog4j2EcsLayoutTest.java index 7182512e..9b58c416 100644 --- a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/AbstractLog4j2EcsLayoutTest.java +++ b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/AbstractLog4j2EcsLayoutTest.java @@ -25,14 +25,12 @@ package co.elastic.logging.log4j2; import co.elastic.logging.AbstractEcsLoggingTest; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; import org.apache.logging.log4j.ThreadContext; import org.apache.logging.log4j.core.Logger; import org.apache.logging.log4j.message.ObjectMessage; -import org.apache.logging.log4j.message.StringMapMessage; import org.apache.logging.log4j.test.appender.ListAppender; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -77,20 +75,6 @@ void testMarker() throws Exception { TextNode.valueOf("grandchild")); } - @Test - void testMapMessage() throws Exception { - root.info(new StringMapMessage().with("message", "foo").with("foo", "bar")); - JsonNode log = getLastLogLine(); - assertThat(log.get("message").textValue()).isEqualTo("foo"); - assertThat(log.get("foo").textValue()).isEqualTo("bar"); - } - - @Test - void testParameterizedStructuredMessage() throws Exception { - root.info(ParameterizedStructuredMessage.of("hello {}", "world").with("foo", "bar")); - assertThat(getLastLogLine().get("message").textValue()).isEqualTo("hello world"); - assertThat(getLastLogLine().get("foo").textValue()).isEqualTo("bar"); - } @Test void testCustomPatternConverter() throws Exception { @@ -98,21 +82,12 @@ void testCustomPatternConverter() throws Exception { assertThat(getLastLogLine().get("custom").textValue()).isEqualTo("foo"); } - @Test - void testJsonMessageObject() throws Exception { - root.info(new ObjectMessage(new TestClass("foo", 42, true))); - - assertThat(getLastLogLine().get("foo").textValue()).isEqualTo("foo"); - assertThat(getLastLogLine().get("bar").intValue()).isEqualTo(42); - assertThat(getLastLogLine().get("baz").booleanValue()).isEqualTo(true); - } - @Test void testJsonMessageArray() throws Exception { root.info(new ObjectMessage(List.of("foo", "bar"))); assertThat(getLastLogLine().get("message").isArray()).isFalse(); - assertThat(getLastLogLine().get("message").textValue()).isEqualTo("[\"foo\",\"bar\"]"); + assertThat(getLastLogLine().get("message").textValue()).contains("foo", "bar"); } @Test @@ -138,45 +113,6 @@ void testJsonMessageBoolean() throws Exception { assertThat(getLastLogLine().get("message").textValue()).isEqualTo("true"); } - public static class TestClass { - String foo; - int bar; - boolean baz; - - private TestClass() { - } - - private TestClass(String foo, int bar, boolean baz) { - this.foo = foo; - this.bar = bar; - this.baz = baz; - } - - public String getFoo() { - return foo; - } - - public void setFoo(String foo) { - this.foo = foo; - } - - public int getBar() { - return bar; - } - - public void setBar(int bar) { - this.bar = bar; - } - - public boolean isBaz() { - return baz; - } - - public void setBaz(boolean baz) { - this.baz = baz; - } - } - @Override public boolean putMdc(String key, String value) { ThreadContext.put(key, value); diff --git a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/CurrentLog4j2EcsLayoutTest.java b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/CurrentLog4j2EcsLayoutTest.java new file mode 100644 index 00000000..6e7fb96d --- /dev/null +++ b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/CurrentLog4j2EcsLayoutTest.java @@ -0,0 +1,97 @@ +/*- + * #%L + * Java ECS logging + * %% + * Copyright (C) 2019 - 2020 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + * #L% + */ +package co.elastic.logging.log4j2; + +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.logging.log4j.message.ObjectMessage; +import org.apache.logging.log4j.message.StringMapMessage; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CurrentLog4j2EcsLayoutTest extends Log4j2EcsLayoutTest { + @Test + void testMapMessage() throws Exception { + root.info(new StringMapMessage().with("message", "foo").with("foo", "bar")); + JsonNode log = getLastLogLine(); + assertThat(log.get("message").textValue()).isEqualTo("foo"); + assertThat(log.get("foo").textValue()).isEqualTo("bar"); + } + + @Test + void testParameterizedStructuredMessage() throws Exception { + root.info(ParameterizedStructuredMessage.of("hello {}", "world").with("foo", "bar")); + assertThat(getLastLogLine().get("message").textValue()).isEqualTo("hello world"); + assertThat(getLastLogLine().get("foo").textValue()).isEqualTo("bar"); + } + + @Test + void testJsonMessageObject() throws Exception { + root.info(new ObjectMessage(new TestClass("foo", 42, true))); + + assertThat(getLastLogLine().get("foo").textValue()).isEqualTo("foo"); + assertThat(getLastLogLine().get("bar").intValue()).isEqualTo(42); + assertThat(getLastLogLine().get("baz").booleanValue()).isEqualTo(true); + } + + public static class TestClass { + String foo; + int bar; + boolean baz; + + private TestClass() { + } + + private TestClass(String foo, int bar, boolean baz) { + this.foo = foo; + this.bar = bar; + this.baz = baz; + } + + public String getFoo() { + return foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public int getBar() { + return bar; + } + + public void setBar(int bar) { + this.bar = bar; + } + + public boolean isBaz() { + return baz; + } + + public void setBaz(boolean baz) { + this.baz = baz; + } + } +} diff --git a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/Log4j2EcsLayoutTest.java b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/Log4j2EcsLayoutTest.java index 3b55260e..6d4f9504 100644 --- a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/Log4j2EcsLayoutTest.java +++ b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/Log4j2EcsLayoutTest.java @@ -39,7 +39,7 @@ import java.io.IOException; -class Log4j2EcsLayoutTest extends AbstractLog4j2EcsLayoutTest { +abstract class Log4j2EcsLayoutTest extends AbstractLog4j2EcsLayoutTest { private static ConfigurationFactory configFactory = new BasicConfigurationFactory(); private LoggerContext ctx; @@ -91,7 +91,7 @@ void setUp() { @Override void tearDown() throws Exception { super.tearDown(); - ctx.close(); + ctx.stop(); } @Override diff --git a/log4j2-legacy-tests/pom.xml b/log4j2-legacy-tests/pom.xml new file mode 100644 index 00000000..27ed7978 --- /dev/null +++ b/log4j2-legacy-tests/pom.xml @@ -0,0 +1,62 @@ + + + + ecs-logging-java-parent + co.elastic.logging + 0.5.3-SNAPSHOT + + 4.0.0 + + log4j2-legacy-tests + + + 2.6 + ${project.basedir}/.. + false + + + + + org.apache.logging.log4j + log4j-core + ${version.log4j.legacy} + test + + + org.apache.logging.log4j + log4j-core + ${version.log4j.legacy} + test-jar + test + + + ${project.groupId} + ecs-logging-core + ${project.version} + test-jar + test + + + ${project.groupId} + log4j2-ecs-layout + ${project.version} + test + + + ${project.groupId} + log4j2-ecs-layout + ${project.version} + test-jar + test + + + com.fasterxml.jackson.core + jackson-databind + [2.9.10.3,) + test + + + + \ No newline at end of file diff --git a/log4j2-legacy-tests/src/test/java/co/elastic/logging/log4j2/LegacyLog4j2EcsLayoutTest.java b/log4j2-legacy-tests/src/test/java/co/elastic/logging/log4j2/LegacyLog4j2EcsLayoutTest.java new file mode 100644 index 00000000..6101ff88 --- /dev/null +++ b/log4j2-legacy-tests/src/test/java/co/elastic/logging/log4j2/LegacyLog4j2EcsLayoutTest.java @@ -0,0 +1,30 @@ +/*- + * #%L + * Java ECS logging + * %% + * Copyright (C) 2019 - 2020 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + * #L% + */ +package co.elastic.logging.log4j2; + +public class LegacyLog4j2EcsLayoutTest extends Log4j2EcsLayoutTest { + + +} diff --git a/pom.xml b/pom.xml index 85100bdf..cc92984e 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,7 @@ logback-ecs-encoder jul-ecs-formatter jboss-logmanager-ecs-formatter + log4j2-legacy-tests pom 2019 @@ -54,7 +55,7 @@ UTF-8 UTF-8 ${project.basedir} - 2.12.0 + false @@ -123,6 +124,12 @@ v@{project.version} + + maven-deploy-plugin + + ${maven-deploy-plugin.skip} + + org.apache.maven.plugins maven-source-plugin