From 04c4a4bd0bd0fe2ba54279765b04d02df542c5bb Mon Sep 17 00:00:00 2001 From: Rafal Foltynski Date: Sun, 2 Dec 2018 18:27:49 +0100 Subject: [PATCH] JacksonJsonLayout added, Log4j 2 and FasterXML Jackson dependencies upgraded * layout implements ItemSourceLayout * layout allows to output customisations via FasterXML Jackson mixins * LogEventJacksonJsonMixIn (modified log4j-core:LogEventJsonMixIn) was added to prevent #9 (visit javadoc for full list of changes) * this layout is now used by default --- log4j2-elasticsearch-core/pom.xml | 11 +- .../jackson/ExtendedLog4j2JsonModule.java | 46 ++++ .../jackson/LogEventJacksonJsonMixIn.java | 129 ++++++++++ .../elasticsearch/ElasticsearchAppender.java | 2 +- .../elasticsearch/ItemSourceFactory.java | 2 + .../JacksonAfterburnerModuleConfigurer.java | 17 ++ .../elasticsearch/JacksonJsonLayout.java | 171 +++++++++++++ .../log4j2/elasticsearch/JacksonMixIn.java | 101 ++++++++ .../StringItemSourceFactory.java | 4 + .../ElasticsearchAppenderTest.java | 2 +- ...acksonAfterburnerModuleConfigurerTest.java | 35 +++ .../elasticsearch/JacksonJsonLayoutTest.java | 237 ++++++++++++++++++ .../elasticsearch/JacksonMixInTest.java | 124 +++++++++ .../src/test/resources/log4j2.xml | 1 + log4j2-elasticsearch-jest/pom.xml | 10 +- .../log4j/core/jackson/TestLogEventMixIn.java | 129 ++++++++++ .../src/test/resources/log4j2.xml | 9 +- pom.xml | 36 +++ 18 files changed, 1049 insertions(+), 17 deletions(-) create mode 100644 log4j2-elasticsearch-core/src/main/java/org/apache/logging/log4j/core/jackson/ExtendedLog4j2JsonModule.java create mode 100644 log4j2-elasticsearch-core/src/main/java/org/apache/logging/log4j/core/jackson/LogEventJacksonJsonMixIn.java create mode 100644 log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonAfterburnerModuleConfigurer.java create mode 100644 log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonJsonLayout.java create mode 100644 log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonMixIn.java create mode 100644 log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonAfterburnerModuleConfigurerTest.java create mode 100644 log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonJsonLayoutTest.java create mode 100644 log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonMixInTest.java create mode 100644 log4j2-elasticsearch-jest/src/test/java/org/apache/logging/log4j/core/jackson/TestLogEventMixIn.java diff --git a/log4j2-elasticsearch-core/pom.xml b/log4j2-elasticsearch-core/pom.xml index fa6c92d9..e80f78b2 100644 --- a/log4j2-elasticsearch-core/pom.xml +++ b/log4j2-elasticsearch-core/pom.xml @@ -16,30 +16,30 @@ org.apache.logging.log4j log4j-core provided - 2.8.1 org.apache.logging.log4j log4j-api provided - 2.8.1 com.fasterxml.jackson.core jackson-databind - 2.8.4 provided com.fasterxml.jackson.core jackson-core - 2.8.4 provided com.fasterxml.jackson.core jackson-annotations - 2.8.0 + provided + + + com.fasterxml.jackson.module + jackson-module-afterburner provided @@ -75,6 +75,7 @@ org.apache.maven.plugins maven-jar-plugin + 3.0.2 diff --git a/log4j2-elasticsearch-core/src/main/java/org/apache/logging/log4j/core/jackson/ExtendedLog4j2JsonModule.java b/log4j2-elasticsearch-core/src/main/java/org/apache/logging/log4j/core/jackson/ExtendedLog4j2JsonModule.java new file mode 100644 index 00000000..24883389 --- /dev/null +++ b/log4j2-elasticsearch-core/src/main/java/org/apache/logging/log4j/core/jackson/ExtendedLog4j2JsonModule.java @@ -0,0 +1,46 @@ +package org.apache.logging.log4j.core.jackson; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2018 Rafal Foltynski + * %% + * Licensed 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% + */ + +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.impl.ExtendedStackTraceElement; +import org.apache.logging.log4j.core.impl.ThrowableProxy; + +public class ExtendedLog4j2JsonModule extends SimpleModule { + + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + + context.setMixInAnnotations(StackTraceElement.class, StackTraceElementMixIn.class); + context.setMixInAnnotations(Marker.class, MarkerMixIn.class); + context.setMixInAnnotations(Level.class, LevelMixIn.class); + context.setMixInAnnotations(ExtendedStackTraceElement.class, ExtendedStackTraceElementMixIn.class); + context.setMixInAnnotations(ThrowableProxy.class, ThrowableProxyMixIn.class); + + // https://github.com/rfoltyns/log4j2-elasticsearch/issues/9, timeMillis can't be ignored + context.setMixInAnnotations(LogEvent.class, LogEventJacksonJsonMixIn.class); + + } +} diff --git a/log4j2-elasticsearch-core/src/main/java/org/apache/logging/log4j/core/jackson/LogEventJacksonJsonMixIn.java b/log4j2-elasticsearch-core/src/main/java/org/apache/logging/log4j/core/jackson/LogEventJacksonJsonMixIn.java new file mode 100644 index 00000000..6679677d --- /dev/null +++ b/log4j2-elasticsearch-core/src/main/java/org/apache/logging/log4j/core/jackson/LogEventJacksonJsonMixIn.java @@ -0,0 +1,129 @@ +package org.apache.logging.log4j.core.jackson; + +/* + * 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. + * + * MODIFICATIONS: + * rfoltyns: + * - timeInMillis not ignored anymore (patch for log4j-core:2.11+) + * - nanoTime, parameterCount, formattedMessage loggerFqcn, source, threadId, threadPriority, endOfBatch, instant ignored + * - XML-related annotations removed + * - setters removed + * - JsonDeserialize annotations removed + * - JsonFilter removed + * + */ + +import java.util.Map; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.ThreadContext.ContextStack; +import org.apache.logging.log4j.core.time.Instant; +import org.apache.logging.log4j.util.ReadOnlyStringMap; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.impl.ThrowableProxy; +import org.apache.logging.log4j.message.Message; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +@JsonPropertyOrder({ "timeMillis", "loggerName", "level", "marker", "message", "thrown", "threadName"}) +public abstract class LogEventJacksonJsonMixIn implements LogEvent { + + private static final long serialVersionUID = 1L; + + @JsonIgnore + @Override + public abstract Map getContextMap(); + + @JsonIgnore + @Override + public abstract ReadOnlyStringMap getContextData(); + + @JsonIgnore + @Override + public abstract ContextStack getContextStack(); + + @JsonProperty + @Override + public abstract Level getLevel(); + + @JsonIgnore + @Override + public abstract String getLoggerFqcn(); + + @JsonProperty + @Override + public abstract String getLoggerName(); + + @JsonProperty(JsonConstants.ELT_MARKER) + @Override + public abstract Marker getMarker(); + + @JsonProperty(JsonConstants.ELT_MESSAGE) + @JsonSerialize(using = MessageSerializer.class) + @Override + public abstract Message getMessage(); + + @JsonIgnore + @Override + public abstract StackTraceElement getSource(); + + @JsonIgnore + @Override + public abstract long getThreadId(); + + @JsonProperty("thread") + @Override + public abstract String getThreadName(); + + @JsonIgnore + @Override + public abstract int getThreadPriority(); + + @JsonIgnore + @Override + public abstract Throwable getThrown(); + + @JsonProperty(JsonConstants.ELT_THROWN) + @Override + public abstract ThrowableProxy getThrownProxy(); + + @JsonProperty + @Override + public abstract long getTimeMillis(); + + @JsonIgnore + @Override + public abstract boolean isEndOfBatch(); + + @JsonIgnore + @Override + public abstract boolean isIncludeLocation(); + + @JsonIgnore + @Override + public abstract long getNanoTime(); + + @JsonIgnore + @Override + public abstract Instant getInstant(); + +} + diff --git a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ElasticsearchAppender.java b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ElasticsearchAppender.java index cfba6b30..bd21c3a9 100644 --- a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ElasticsearchAppender.java +++ b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ElasticsearchAppender.java @@ -117,7 +117,7 @@ public ElasticsearchAppender build() { } if (layout == null) { - layout = JsonLayout.newBuilder().setCompact(true).build(); + layout = JacksonJsonLayout.newBuilder().build(); } return new ElasticsearchAppender(name, filter, layout, ignoreExceptions, batchDelivery, messageOnly, indexNameFormatter); diff --git a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ItemSourceFactory.java b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ItemSourceFactory.java index c9d57721..0ab79795 100644 --- a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ItemSourceFactory.java +++ b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ItemSourceFactory.java @@ -24,6 +24,8 @@ public interface ItemSourceFactory { + String ELEMENT_TYPE = "itemSourceFactory"; + /** * Indicates whether {@link ItemSource} lifecycle has to be taken care of * diff --git a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonAfterburnerModuleConfigurer.java b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonAfterburnerModuleConfigurer.java new file mode 100644 index 00000000..53bbaf98 --- /dev/null +++ b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonAfterburnerModuleConfigurer.java @@ -0,0 +1,17 @@ +package org.appenders.log4j2.elasticsearch; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.afterburner.AfterburnerModule; + +/** + * Wraps {@link AfterburnerModule} configuration to avoid {@link ClassNotFoundException} + * when {@code {@link org.appenders.log4j2.elasticsearch.JacksonJsonLayout.Builder#withAfterburner(boolean)}} is set to false + * and com.fasterxml.jackson.module:jackson-module-afterburner dependency was not provided + */ +final class JacksonAfterburnerModuleConfigurer { + + void configure(ObjectMapper objectMapper) { + objectMapper.registerModule(new AfterburnerModule()); + } + +} \ No newline at end of file diff --git a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonJsonLayout.java b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonJsonLayout.java new file mode 100644 index 00000000..a8e9c1a0 --- /dev/null +++ b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonJsonLayout.java @@ -0,0 +1,171 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2018 Rafal Foltynski + * %% + * Licensed 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% + */ + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.util.MinimalPrettyPrinter; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Node; +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.PluginElement; +import org.apache.logging.log4j.core.jackson.ExtendedLog4j2JsonModule; +import org.apache.logging.log4j.core.layout.AbstractLayout; +import org.apache.logging.log4j.message.Message; + +import java.util.Arrays; +import java.util.List; + +/** + * Allows to customize serialization of incoming events + */ +@Plugin(name = JacksonJsonLayout.PLUGIN_NAME, category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) +public class JacksonJsonLayout extends AbstractLayout implements ItemSourceLayout { + + public static final String PLUGIN_NAME = "JacksonJsonLayout"; + + private final ObjectWriter objectWriter; + private final ItemSourceFactory itemSourceFactory; + + protected JacksonJsonLayout(Configuration config, ObjectWriter configuredWriter, ItemSourceFactory itemSourceFactory) { + super(config, null, null); + this.objectWriter = configuredWriter; + this.itemSourceFactory = itemSourceFactory; + } + + @Override + public String getContentType() { + return "application/json"; + } + + @Override + public byte[] toByteArray(LogEvent event) { + throw new UnsupportedOperationException("Cannot return unwrapped byte array. Use ItemSource based API"); + } + + @Override + public final ItemSource toSerializable(LogEvent event) { + return serialize(event); + } + + @Override + public final ItemSource serialize(LogEvent event) { + return itemSourceFactory.create(event, objectWriter); + } + + @Override + public final ItemSource serialize(Message message) { + return itemSourceFactory.create(message, objectWriter); + } + + @PluginBuilderFactory + public static JacksonJsonLayout.Builder newBuilder() { + return new JacksonJsonLayout.Builder(); + } + + public static class Builder extends org.apache.logging.log4j.core.layout.AbstractLayout.Builder implements org.apache.logging.log4j.core.util.Builder { + + /** + * Default: {@link StringItemSourceFactory} + */ + static final ItemSourceFactory DEFAULT_SOURCE_FACTORY = StringItemSourceFactory.newBuilder().build(); + + @PluginElement(ItemSourceFactory.ELEMENT_TYPE) + private ItemSourceFactory itemSourceFactory = DEFAULT_SOURCE_FACTORY; + + @PluginElement(JacksonMixIn.ELEMENT_TYPE) + private JacksonMixIn[] mixins = new JacksonMixIn[0]; + + @PluginBuilderAttribute("afterburner") + private boolean useAfterburner; + + @Override + public JacksonJsonLayout build() { + return new JacksonJsonLayout( + getConfiguration(), + createConfiguredWriter(Arrays.asList(mixins)), + itemSourceFactory + ); + } + + protected ObjectWriter createConfiguredWriter(List mixins) { + + ObjectMapper objectMapper = createDefaultObjectMapper(); + objectMapper.registerModule(new ExtendedLog4j2JsonModule()); + + if (useAfterburner) { + // com.fasterxml.jackson.module:jackson-module-afterburner required here + new JacksonAfterburnerModuleConfigurer().configure(objectMapper); + } + + for (JacksonMixIn mixin : mixins) { + objectMapper.addMixIn(mixin.getTargetClass(), mixin.getMixInClass()); + } + + return objectMapper.writer(new MinimalPrettyPrinter()); + } + + protected ObjectMapper createDefaultObjectMapper() { + return new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) + .configure(SerializationFeature.CLOSE_CLOSEABLE, false); + } + + /** + * @param itemSourceFactory {@link ItemSource} producer + * @return this + */ + public Builder withItemSourceFactory(ItemSourceFactory itemSourceFactory) { + this.itemSourceFactory = itemSourceFactory; + return this; + } + + /** + * Allows to customize {@link LogEvent} and {@link Message} serialization, + * including user-provided {@link org.apache.logging.log4j.message.ObjectMessage} + * + * @param mixins mixins to be applied + * @return this + */ + public Builder withMixins(JacksonMixIn... mixins) { + this.mixins = mixins; + return this; + } + + /** + * Allows to configure {@link AfterburnerModule} - (de)serialization optimizer + * + * @param useAfterburner if true, {@link AfterburnerModule} will be used, false otherwise + * @return this + */ + public Builder withAfterburner(boolean useAfterburner) { + this.useAfterburner = useAfterburner; + return this; + } + } +} diff --git a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonMixIn.java b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonMixIn.java new file mode 100644 index 00000000..b66797f8 --- /dev/null +++ b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonMixIn.java @@ -0,0 +1,101 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2018 Rafal Foltynski + * %% + * Licensed 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% + */ + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.config.ConfigurationException; +import org.apache.logging.log4j.core.config.Node; +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.util.LoaderUtil; + +@Plugin(name = JacksonMixIn.PLUGIN_NAME, category = Node.CATEGORY, elementType = JacksonMixIn.ELEMENT_TYPE, printObject = true) +public class JacksonMixIn { + + public static final String PLUGIN_NAME = "JacksonMixIn"; + public static final String ELEMENT_TYPE = "jacksonMixIn"; + + private final Class targetClass; + private final Class mixInClass; + + protected JacksonMixIn(Class targetClass, Class mixInClass) { + this.targetClass = targetClass; + this.mixInClass = mixInClass; + } + + public Class getTargetClass() { + return targetClass; + } + + public Class getMixInClass() { + return mixInClass; + } + + @PluginBuilderFactory + public static JacksonMixIn.Builder newBuilder() { + return new JacksonMixIn.Builder(); + } + + public static class Builder implements org.apache.logging.log4j.core.util.Builder { + + @PluginBuilderAttribute("targetClass") + private String targetClassName; + + @PluginBuilderAttribute("mixInClass") + private String mixInClassName; + + @Override + public JacksonMixIn build() { + + Class targetClass = loadClass(targetClassName, "targetClass"); + Class mixInClass = loadClass(mixInClassName, "mixInClass"); + + return new JacksonMixIn(targetClass, mixInClass); + + } + + private Class loadClass(String className, String argName) { + + if (className == null) { + throw new ConfigurationException(String.format("No %s provided for %s", argName, JacksonMixIn.PLUGIN_NAME)); + } + + try { + return LoaderUtil.loadClass(className); + } catch (ClassNotFoundException e) { + throw new ConfigurationException(String.format("Cannot load %s: %s for %s", argName, className, JacksonMixIn.PLUGIN_NAME)); + } + + } + + public Builder withTargetClass(String targetClass) { + this.targetClassName = targetClass; + return this; + } + + public Builder withMixInClass(String mixInClass) { + this.mixInClassName = mixInClass; + return this; + } + + } +} diff --git a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/StringItemSourceFactory.java b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/StringItemSourceFactory.java index d2fadfaa..9d1fa65d 100644 --- a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/StringItemSourceFactory.java +++ b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/StringItemSourceFactory.java @@ -23,14 +23,18 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectWriter; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.Layout; import org.apache.logging.log4j.core.config.Node; import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; import org.apache.logging.log4j.status.StatusLogger; +@Plugin(name = StringItemSourceFactory.PLUGIN_NAME, category = Node.CATEGORY, elementType = ItemSourceFactory.ELEMENT_TYPE, printObject = true) public class StringItemSourceFactory implements ItemSourceFactory { + static final String PLUGIN_NAME = "StringItemSourceAppender"; + private static final Logger LOGGER = StatusLogger.getLogger(); StringItemSourceFactory() { diff --git a/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/ElasticsearchAppenderTest.java b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/ElasticsearchAppenderTest.java index 343838bd..e6be8682 100644 --- a/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/ElasticsearchAppenderTest.java +++ b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/ElasticsearchAppenderTest.java @@ -179,7 +179,7 @@ public void appenderUsesProvidedIndexNameFormatter() { appender.append(logEvent); // then - verify(batchDelivery, times(1)).add(eq("formattedIndexName"), anyString()); + verify(batchDelivery, times(1)).add(eq("formattedIndexName"), any(ItemSource.class)); } private LogEvent createTestLogEvent() { diff --git a/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonAfterburnerModuleConfigurerTest.java b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonAfterburnerModuleConfigurerTest.java new file mode 100644 index 00000000..0992c54f --- /dev/null +++ b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonAfterburnerModuleConfigurerTest.java @@ -0,0 +1,35 @@ +package org.appenders.log4j2.elasticsearch; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import org.appenders.log4j2.elasticsearch.JacksonAfterburnerModuleConfigurer; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class JacksonAfterburnerModuleConfigurerTest { + + @Test + public void configuresAfterburnerModule() { + + // given + JacksonAfterburnerModuleConfigurer configurer = new JacksonAfterburnerModuleConfigurer(); + + ObjectMapper objectMapper = spy(new ObjectMapper()); + + // when + configurer.configure(objectMapper); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(AfterburnerModule.class); + verify(objectMapper).registerModule(captor.capture()); + + assertEquals(AfterburnerModule.class, captor.getValue().getClass()); + + } + +} diff --git a/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonJsonLayoutTest.java b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonJsonLayoutTest.java new file mode 100644 index 00000000..427dd4e2 --- /dev/null +++ b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonJsonLayoutTest.java @@ -0,0 +1,237 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2018 Rafal Foltynski + * %% + * Licensed 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% + */ + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.impl.Log4jLogEvent; +import org.apache.logging.log4j.core.jackson.ExtendedLog4j2JsonModule; +import org.apache.logging.log4j.core.jackson.LogEventJacksonJsonMixIn; +import org.apache.logging.log4j.message.Message; +import org.hamcrest.core.IsInstanceOf; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class JacksonJsonLayoutTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void builderBuildsSuccessfully() { + + // given + JacksonJsonLayout.Builder builder = JacksonJsonLayout.newBuilder(); + + // when + JacksonJsonLayout layout = builder.build(); + + // then + Assert.assertNotNull(layout); + + } + + @Test + public void contentTypeIsNotNull() { + + // given + JacksonJsonLayout layout = JacksonJsonLayout.newBuilder().build(); + + // when + String contentType = layout.getContentType(); + + // then + assertNotNull(contentType); + } + + @Test + public void throwsOnByteArrayCreationAttempt() { + + // given + JacksonJsonLayout layout = JacksonJsonLayout.newBuilder().build(); + + expectedException.expect(UnsupportedOperationException.class); + + // when + layout.toByteArray(new Log4jLogEvent()); + + } + + @Test + public void builderBuildsLayoutWithDefaultItemSourceFactoryIfNotConfigured() { + + // given + JacksonJsonLayout.Builder builder = spy(JacksonJsonLayout.newBuilder()); + JacksonJsonLayout layout = builder.build(); + + LogEvent logEvent = new Log4jLogEvent(); + + // when + ItemSource itemSource = layout.toSerializable(logEvent); + + // then + assertEquals(StringItemSource.class, itemSource.getClass()); + + } + + @Test + public void builderBuildsLayoutWithProvidedItemSourceFactoryIfConfigured() { + + // given + JacksonJsonLayout.Builder builder = spy(JacksonJsonLayout.newBuilder()); + builder.withItemSourceFactory(new LayoutTestItemSourceFactory()); + + JacksonJsonLayout layout = builder.build(); + + LogEvent logEvent = new Log4jLogEvent(); + + // when + ItemSource itemSource = layout.toSerializable(logEvent); + + // then + assertEquals(LayoutTestItemSource.class, itemSource.getClass()); + + } + + @Test + public void builderBuildsMapperWithAfterburnerIfConfigured() { + + // given + JacksonJsonLayout.Builder builder = spy(JacksonJsonLayout.newBuilder()); + builder.withAfterburner(true); + + ObjectMapper objectMapper = Mockito.mock(ObjectMapper.class); + when(builder.createDefaultObjectMapper()).thenReturn(objectMapper); + + // when + builder.build(); + + // then + verify(objectMapper).registerModule(argThat(IsInstanceOf.instanceOf(AfterburnerModule.class))); + + } + + @Test + public void builderBuildsMapperWithMixInsIfConfigured() { + + // given + JacksonJsonLayout.Builder builder = spy(JacksonJsonLayout.newBuilder()); + builder.withMixins(JacksonMixInTest.createDefaultTestBuilder().build()); + + ObjectMapper objectMapper = Mockito.mock(ObjectMapper.class); + when(builder.createDefaultObjectMapper()).thenReturn(objectMapper); + + // when + builder.build(); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(ExtendedLog4j2JsonModule.class); + verify(objectMapper).registerModule(captor.capture()); + + Module.SetupContext setupContext = mock(Module.SetupContext.class); + captor.getValue().setupModule(setupContext); + verify(setupContext).setMixInAnnotations(eq(LogEvent.class), eq(LogEventJacksonJsonMixIn.class)); + + } + + @Test + public void messageSerializationDelegatesToItemSourceFactory() { + + // given + JacksonJsonLayout.Builder builder = spy(JacksonJsonLayout.newBuilder()); + LayoutTestItemSourceFactory itemSourceFactory = spy(new LayoutTestItemSourceFactory()); + builder.withItemSourceFactory(itemSourceFactory); + + JacksonJsonLayout layout = builder.build(); + + LogEvent logEvent = new Log4jLogEvent(); + Message message = logEvent.getMessage(); + + // when + layout.serialize(message); + + // then + verify(itemSourceFactory).create(eq(message), any(ObjectWriter.class)); + + } + + @Test + public void logEventSerializationDelegatesToItemSourceFactory() { + + // given + JacksonJsonLayout.Builder builder = spy(JacksonJsonLayout.newBuilder()); + LayoutTestItemSourceFactory itemSourceFactory = spy(new LayoutTestItemSourceFactory()); + builder.withItemSourceFactory(itemSourceFactory); + + JacksonJsonLayout layout = builder.build(); + + LogEvent logEvent = new Log4jLogEvent(); + + // when + layout.toSerializable(logEvent); + + // then + verify(itemSourceFactory).create(eq(logEvent), any(ObjectWriter.class)); + + } + + public static class LayoutTestItemSourceFactory extends StringItemSourceFactory { + + @Override + public ItemSource create(Object event, ObjectWriter objectWriter) { + try { + return new LayoutTestItemSource(objectWriter.writeValueAsString(event)); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + return null; + } + } + + public static class LayoutTestItemSource extends StringItemSource { + + public LayoutTestItemSource(String source) { + super(source); + } + + } +} diff --git a/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonMixInTest.java b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonMixInTest.java new file mode 100644 index 00000000..31361439 --- /dev/null +++ b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonMixInTest.java @@ -0,0 +1,124 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2018 Rafal Foltynski + * %% + * Licensed 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% + */ + +import org.apache.logging.log4j.core.config.ConfigurationException; +import org.apache.logging.log4j.core.impl.Log4jLogEvent; +import org.apache.logging.log4j.core.jackson.LogEventJacksonJsonMixIn; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class JacksonMixInTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + public static JacksonMixIn.Builder createDefaultTestBuilder() { + JacksonMixIn.Builder builder = JacksonMixIn.newBuilder(); + builder.withMixInClass(LogEventJacksonJsonMixIn.class.getName()); + builder.withTargetClass(Log4jLogEvent.class.getName()); + return builder; + } + + @Test + public void buiderSucceedsOnValidConfig() { + + // given + JacksonMixIn.Builder builder = createDefaultTestBuilder(); + + // when + JacksonMixIn jacksonMixIn = builder.build(); + + // then + assertNotNull(jacksonMixIn); + assertEquals(Log4jLogEvent.class, jacksonMixIn.getTargetClass()); + assertEquals(LogEventJacksonJsonMixIn.class, jacksonMixIn.getMixInClass()); + } + + @Test + public void builderThrowsOnNullTargetClass() { + + // given + JacksonMixIn.Builder builder = createDefaultTestBuilder(); + builder.withTargetClass(null); + + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage("No targetClass provided for " + JacksonMixIn.PLUGIN_NAME); + + // when + builder.build(); + + } + + @Test + public void builderThrowsOnNullMixInClass() { + + // given + JacksonMixIn.Builder builder = createDefaultTestBuilder(); + builder.withMixInClass(null); + + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage("No mixInClass provided for " + JacksonMixIn.PLUGIN_NAME); + + // when + builder.build(); + + } + + @Test + public void builderThrowsOnMixInClassNotFound() { + + // given + JacksonMixIn.Builder builder = createDefaultTestBuilder(); + String mixInClass = "org.appenders.test.NonExistingClass"; + builder.withMixInClass(mixInClass); + + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage("Cannot load mixInClass"); + expectedException.expectMessage(mixInClass); + + // when + builder.build(); + + } + + @Test + public void builderThrowsOnTargetClassNotFound() { + + // given + JacksonMixIn.Builder builder = createDefaultTestBuilder(); + String targetClass = "org.appenders.test.NonExistingClass"; + builder.withTargetClass(targetClass); + + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage("Cannot load targetClass"); + expectedException.expectMessage(targetClass); + + // when + builder.build(); + + } + +} diff --git a/log4j2-elasticsearch-core/src/test/resources/log4j2.xml b/log4j2-elasticsearch-core/src/test/resources/log4j2.xml index c4e97318..93a5a6b9 100644 --- a/log4j2-elasticsearch-core/src/test/resources/log4j2.xml +++ b/log4j2-elasticsearch-core/src/test/resources/log4j2.xml @@ -1,3 +1,4 @@ + org.appenders.log4j diff --git a/log4j2-elasticsearch-jest/src/test/java/org/apache/logging/log4j/core/jackson/TestLogEventMixIn.java b/log4j2-elasticsearch-jest/src/test/java/org/apache/logging/log4j/core/jackson/TestLogEventMixIn.java new file mode 100644 index 00000000..f1dae769 --- /dev/null +++ b/log4j2-elasticsearch-jest/src/test/java/org/apache/logging/log4j/core/jackson/TestLogEventMixIn.java @@ -0,0 +1,129 @@ +package org.apache.logging.log4j.core.jackson; + +/* + * 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. + * + * MODIFICATIONS: + * rfoltyns: + * - timeInMillis not ignored anymore (patch for log4j-core:2.11+) + * - nanoTime, parameterCount, formattedMessage loggerFqcn, source, threadId, threadPriority, endOfBatch, instant ignored + * - XML-related annotations removed + * - setters removed + * - JsonDeserialize annotations removed + * - JsonFilter removed + * + */ + +import java.util.Map; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.ThreadContext.ContextStack; +import org.apache.logging.log4j.core.time.Instant; +import org.apache.logging.log4j.util.ReadOnlyStringMap; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.impl.ThrowableProxy; +import org.apache.logging.log4j.message.Message; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +@JsonPropertyOrder({ "timeMillis", "loggerName", "level", "marker", "message", "thrown", "threadName"}) +public abstract class TestLogEventMixIn implements LogEvent { + + private static final long serialVersionUID = 1L; + + @JsonIgnore + @Override + public abstract Map getContextMap(); + + @JsonIgnore + @Override + public abstract ReadOnlyStringMap getContextData(); + + @JsonIgnore + @Override + public abstract ContextStack getContextStack(); + + @JsonProperty + @Override + public abstract Level getLevel(); + + @JsonIgnore + @Override + public abstract String getLoggerFqcn(); + + @JsonProperty + @Override + public abstract String getLoggerName(); + + @JsonProperty(JsonConstants.ELT_MARKER) + @Override + public abstract Marker getMarker(); + + @JsonProperty(JsonConstants.ELT_MESSAGE) + @JsonSerialize(using = MessageSerializer.class) + @Override + public abstract Message getMessage(); + + @JsonIgnore + @Override + public abstract StackTraceElement getSource(); + + @JsonProperty + @Override + public abstract long getThreadId(); + + @JsonProperty("thread") + @Override + public abstract String getThreadName(); + + @JsonIgnore + @Override + public abstract int getThreadPriority(); + + @JsonIgnore + @Override + public abstract Throwable getThrown(); + + @JsonProperty(JsonConstants.ELT_THROWN) + @Override + public abstract ThrowableProxy getThrownProxy(); + + @JsonProperty + @Override + public abstract long getTimeMillis(); + + @JsonIgnore + @Override + public abstract boolean isEndOfBatch(); + + @JsonIgnore + @Override + public abstract boolean isIncludeLocation(); + + @JsonIgnore + @Override + public abstract long getNanoTime(); + + @JsonIgnore + @Override + public abstract Instant getInstant(); + +} + diff --git a/log4j2-elasticsearch-jest/src/test/resources/log4j2.xml b/log4j2-elasticsearch-jest/src/test/resources/log4j2.xml index 11700e9c..021ee00d 100644 --- a/log4j2-elasticsearch-jest/src/test/resources/log4j2.xml +++ b/log4j2-elasticsearch-jest/src/test/resources/log4j2.xml @@ -28,6 +28,11 @@ + + + + @@ -53,14 +58,14 @@ --> - + - + diff --git a/pom.xml b/pom.xml index 51ea0128..3c2df931 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,41 @@ log4j2-elasticsearch-core ${project.version} + + org.apache.logging.log4j + log4j-core + 2.11.1 + + + org.apache.logging.log4j + log4j-api + 2.11.1 + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.11.1 + + + com.fasterxml.jackson.core + jackson-core + 2.9.7 + + + com.fasterxml.jackson.core + jackson-databind + 2.9.7 + + + com.fasterxml.jackson.core + jackson-annotations + 2.9.7 + + + com.fasterxml.jackson.module + jackson-module-afterburner + 2.9.7 + @@ -120,6 +155,7 @@ **/thirdparty/* + **/*MixIn.*