diff --git a/log4j2-elasticsearch-core/README.md b/log4j2-elasticsearch-core/README.md index 1b039ed1..f6dba47d 100644 --- a/log4j2-elasticsearch-core/README.md +++ b/log4j2-elasticsearch-core/README.md @@ -112,7 +112,7 @@ or ``` -NOTE: Be aware that template parsing errors on cluster side DO NOT prevent plugin from loading - error is logged on client side and startup continues. +NOTE: Be aware that template parsing errors on cluster side MAY NOT prevent plugin from loading - error is logged on client side and startup continues. ### Message output @@ -120,12 +120,11 @@ There are numerous ways to generate JSON output: #### JacksonJsonLayout -(default) - -Since 1.3, [org.appenders.log4j2.elasticsearch.JacksonJsonLayout](https://github.com/rfoltyns/log4j2-elasticsearch/blob/master/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonJsonLayout.java) is the default implemetation of [ItemSourceLayout](https://github.com/rfoltyns/log4j2-elasticsearch/blob/master/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ItemSourceLayout.java). It will serialize LogEvent using Jackson mapper configured with a set of default and (optional) user-provided mixins (see: [JacksonMixInAnnotations docs](https://github.com/FasterXML/jackson-docs/wiki/JacksonMixInAnnotations)). +Since 1.3, [org.appenders.log4j2.elasticsearch.JacksonJsonLayout](https://github.com/rfoltyns/log4j2-elasticsearch/blob/master/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonJsonLayout.java) - implemetation of [ItemSourceLayout](https://github.com/rfoltyns/log4j2-elasticsearch/blob/master/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ItemSourceLayout.java) - can be specified to handle incoming LogEvent(s). It will serialize LogEvent(s) using Jackson mapper configured with a set of default and (optional) user-provided mixins (see: [JacksonMixInAnnotations docs](https://github.com/FasterXML/jackson-docs/wiki/JacksonMixInAnnotations)) and (since 1.4) [Virtual Properties](https://github.com/rfoltyns/log4j2-elasticsearch/blob/master/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/VirtualProperty.java). Default set of mixins limits LogEvent output by shrinking serialized properties list to a 'reasonable minimum'. -Customization of all aspects of LogEvent and Message output are allowed using `JacksonMixIn` elements (see: [JacksonMixInAnnotations docs](https://github.com/FasterXML/jackson-docs/wiki/JacksonMixInAnnotations)) elements. +Additional properties can be specified with [VirtualProperty](#virtual-properties) elements. +Customizations of all aspects of LogEvent and Message output are allowed using `JacksonMixIn` elements (see: [JacksonMixInAnnotations docs](https://github.com/FasterXML/jackson-docs/wiki/JacksonMixInAnnotations)) elements. Furthermore, [ItemSource API](#itemsource-api) allows to use pooled [ByteByfItemSource](https://github.com/rfoltyns/log4j2-elasticsearch/blob/master/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ByteBufItemSource.java) payloads. Pooling is optional. @@ -133,7 +132,8 @@ Config property | Type | Required | Default | Description ------------ | ------------- | ------------- | ------------- | ------------- afterburner | Attribute | no | false | if `true`, `com.fasterxml.jackson.module:jackson-module-afterburner` will be used to optimize (de)serialization. Since this dependency is in `provided` scope by default, it MUST be declared explicitly. mixins | Element(s) | no | None | Array of `JacksonMixIn` elements. Can be used to override default serialization of LogEvent, Message and related objects -itemSourceFactory | Element | no | `StringItemSourceFactory` | `ItemSourceFactory` used to create wrappers for serialized items. `StringItemSourceFactory` and `PooledItemSourceFactory` are available +virtualProperties (since 1.4) | Element(s) | no | None | Array of `VirtualProperty` elements. Similar to `KeyValuePair`, can be used to define properties resolvable on the fly, not available in LogEvent(s). +itemSourceFactory | Element | yes (since 1.4) | n/a | `ItemSourceFactory` used to create wrappers for serialized items. `StringItemSourceFactory` and `PooledItemSourceFactory` are available Default output: @@ -147,6 +147,7 @@ Example: + ... @@ -154,13 +155,25 @@ Example: Custom `org.appenders.log4j2.elasticsearch.ItemSourceLayout` can be provided to appender config to use any other serialization mechanism. +##### Virtual Properties + +Since 1.4, `VirtualProperty` elements (`KeyValuePair` on steroids) can be appended to serialized objects. + +Config property | Type | Required | Default | Description +------------ | ------------- | ------------- | ------------- | ------------- +name | Attribute | yes | n/a | +value | Attribute | yes | n/a | Static value or contextual variable resolvable with Log4j2 Lookups. +dynamic | Attribute | no | false | if `true`, indicates that value may change over time and should be resolved on every serialization (see [Log4jLookup](https://github.com/rfoltyns/log4j2-elasticsearch/blob/master/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/Log4jLookup.java)). Otherwise, will be resolved only on startup. + +Custom lookup can implemented with [ValueResolver](https://github.com/rfoltyns/log4j2-elasticsearch/blob/master/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ValueResolver.java). + #### Log4j2 JsonLayout -JsonLayout will serialize LogEvent using Jackson mapper configured in log4j-core. Custom `org.apache.logging.log4j.core.layout.AbstractLayout` can be provided to appender config to use any other serialization mechanism. +`JsonLayout` will serialize LogEvent using Jackson mapper configured in log4j-core. Custom `org.apache.logging.log4j.core.layout.AbstractLayout` can be provided to appender config to use any other serialization mechanism. Output may vary across different Log4j2 versions (see: #9) #### Raw log message -`messageOnly="true"` can be configured for all the layouts mentioned above to make use of user provided (or default) `org.apache.logging.log4j.message.Message.getFormattedMessage()` implementation. +`messageOnly="true"` can be configured for all layouts mentioned above to make use of user provided (or default) `org.apache.logging.log4j.message.Message.getFormattedMessage()` implementation. Raw log message MUST: * be logged with Logger that uses `org.apache.logging.log4j.message.MessageFactory` that serializes logged object to a valid JSON output 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 index 4f77eb76..7514de6b 100644 --- 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 @@ -29,6 +29,7 @@ import java.util.Map; +import com.fasterxml.jackson.databind.annotation.JsonAppend; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.ThreadContext.ContextStack; @@ -42,9 +43,18 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.appenders.log4j2.elasticsearch.VirtualPropertiesWriter; +import org.appenders.log4j2.elasticsearch.VirtualProperty; @JsonPropertyOrder({ "timeMillis", "loggerName", "level", "marker", "message", "thrown", "threadName"}) @JsonSerialize(as = LogEvent.class) +@JsonAppend(props = { + @JsonAppend.Prop( + name = "virtualProperties", // irrelevant at runtime + type = VirtualProperty[].class, // irrelevant at runtime + value = VirtualPropertiesWriter.class + ) +}) public abstract class LogEventJacksonJsonMixIn implements LogEvent { private static final long serialVersionUID = 1L; 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 713d8689..b994587c 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 @@ -35,10 +35,6 @@ import org.apache.logging.log4j.core.layout.AbstractLayout; import org.apache.logging.log4j.core.layout.AbstractStringLayout; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -123,7 +119,7 @@ public ElasticsearchAppender build() { } if (layout == null) { - layout = JacksonJsonLayout.newBuilder().build(); + throw new ConfigurationException("No layout provided for Elasticsearch appender"); } return new ElasticsearchAppender(name, filter, layout, ignoreExceptions, batchDelivery, messageOnly, indexNameFormatter); diff --git a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonHandlerInstantiator.java b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonHandlerInstantiator.java new file mode 100644 index 00000000..25fdd71f --- /dev/null +++ b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/JacksonHandlerInstantiator.java @@ -0,0 +1,89 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2019 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.DeserializationConfig; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; + +public class JacksonHandlerInstantiator extends HandlerInstantiator { + + private final VirtualProperty[] virtualProperties; + private final ValueResolver valueResolver; + private VirtualPropertiesWriter instance; + + /** + * @param virtualProperties properties to be appended + * @param valueResolver used to resolve properties if {@link VirtualProperty#isDynamic()} is true + */ + public JacksonHandlerInstantiator(VirtualProperty[] virtualProperties, ValueResolver valueResolver) { + this.virtualProperties = virtualProperties; + this.valueResolver = valueResolver; + } + + @Override + public JsonDeserializer deserializerInstance(DeserializationConfig config, Annotated annotated, Class deserClass) { + return null; + } + + @Override + public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class keyDeserClass) { + return null; + } + + @Override + public JsonSerializer serializerInstance(SerializationConfig config, Annotated annotated, Class serClass) { + return null; + } + + @Override + public TypeResolverBuilder typeResolverBuilderInstance(MapperConfig config, Annotated annotated, Class builderClass) { + return null; + } + + @Override + public TypeIdResolver typeIdResolverInstance(MapperConfig config, Annotated annotated, Class resolverClass) { + return null; + } + + /** + * Allows to inject {@link VirtualPropertiesWriter} in order to resolve and write {@link VirtualProperty}-ies + * + * @return Shared {@link VirtualPropertiesWriter} + */ + @Override + public VirtualPropertiesWriter virtualPropertyWriterInstance(MapperConfig config, Class implClass) { + if (instance == null) { + instance = new VirtualPropertiesWriter( + virtualProperties, + valueResolver); + } + return instance; + } + +} 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 index ed4e66d1..c847c301 100644 --- 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 @@ -25,11 +25,13 @@ import com.fasterxml.jackson.core.util.MinimalPrettyPrinter; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationConfig; 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.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; @@ -43,7 +45,7 @@ import java.util.List; /** - * Allows to customize serialization of incoming events + * Allows to customize serialization of incoming events. See {@link Builder} API docs for more details */ @Plugin(name = JacksonJsonLayout.PLUGIN_NAME, category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) public class JacksonJsonLayout extends AbstractLayout implements ItemSourceLayout, LifeCycle { @@ -104,11 +106,19 @@ public static class Builder extends org.apache.logging.log4j.core.layout.Abstrac @PluginElement(JacksonMixIn.ELEMENT_TYPE) private JacksonMixIn[] mixins = new JacksonMixIn[0]; + @PluginElement("VirtualProperty") + private VirtualProperty[] virtualProperties = new VirtualProperty[0]; + @PluginBuilderAttribute("afterburner") private boolean useAfterburner; @Override public JacksonJsonLayout build() { + + if (getConfiguration() == null) { + throw new ConfigurationException("No Configuration instance provided for " + PLUGIN_NAME); + } + return new JacksonJsonLayout( getConfiguration(), createConfiguredWriter(Arrays.asList(mixins)), @@ -130,7 +140,29 @@ protected ObjectWriter createConfiguredWriter(List mixins) { objectMapper.addMixIn(mixin.getTargetClass(), mixin.getMixInClass()); } + for (VirtualProperty property : virtualProperties) { + if (!property.isDynamic()) { + property.setValue(createValueResolver().resolve(property.getValue())); + } + } + + SerializationConfig customConfig = objectMapper.getSerializationConfig() + .with(new JacksonHandlerInstantiator( + virtualProperties, + createValueResolver() + )); + + objectMapper.setConfig(customConfig); + return objectMapper.writer(new MinimalPrettyPrinter()); + + } + + /** + * @return resolver used when {@link VirtualProperty}(-ies) configured + */ + protected ValueResolver createValueResolver() { + return new Log4j2Lookup(getConfiguration().getStrSubstitutor()); } protected ObjectMapper createDefaultObjectMapper() { @@ -160,6 +192,23 @@ public Builder withMixins(JacksonMixIn... mixins) { return this; } + /** + * Allows to append properties to serialized {@link LogEvent} and {@link Message}. + * + * Non-dynamic properties ({@code VirtualProperty#dynamic == false}) will be resolved on {@link #build()} call. + * + * Dynamic properties ({@code VirtualProperty#isDynamic == true}) will NOT be resolved on {@link #build()} call and resolution will be deferred to underlying {@link VirtualPropertiesWriter}. + * + * Similar to Log4j2 {@code KeyValuePair}. + * + * @param virtualProperties properties to be appended to JSON output + * @return this + */ + public Builder withVirtualProperties(VirtualProperty... virtualProperties) { + this.virtualProperties = virtualProperties; + return this; + } + /** * Allows to configure {@link AfterburnerModule} - (de)serialization optimizer * diff --git a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/Log4j2Lookup.java b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/Log4j2Lookup.java new file mode 100644 index 00000000..b75717f5 --- /dev/null +++ b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/Log4j2Lookup.java @@ -0,0 +1,52 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2019 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.lookup.StrSubstitutor; + +public class Log4j2Lookup implements ValueResolver { + + private final StrSubstitutor strSubstitutor; + + public Log4j2Lookup(StrSubstitutor strSubstitutor) { + this.strSubstitutor = strSubstitutor; + } + + /** + * Resolves given {@link VirtualProperty} if {@link VirtualProperty#isDynamic()} is true + * + * @param property property to resolve + * @return resolved value + */ + @Override + public String resolve(VirtualProperty property) { + if (property.isDynamic()) { + return resolve(property.getValue()); + } + return property.getValue(); + } + + @Override + public String resolve(String unresolved) { + return strSubstitutor.replace(unresolved); + } + +} diff --git a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ValueResolver.java b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ValueResolver.java new file mode 100644 index 00000000..5b67925f --- /dev/null +++ b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/ValueResolver.java @@ -0,0 +1,29 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2019 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% + */ + +public interface ValueResolver { + + String resolve(String unresolved); + + String resolve(VirtualProperty property); + +} diff --git a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/VirtualPropertiesWriter.java b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/VirtualPropertiesWriter.java new file mode 100644 index 00000000..1e0cb110 --- /dev/null +++ b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/VirtualPropertiesWriter.java @@ -0,0 +1,108 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2019 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.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonAppend; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.introspect.AnnotatedClass; +import com.fasterxml.jackson.databind.introspect.AnnotationCollector; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter; +import com.fasterxml.jackson.databind.util.Annotations; + +/** + * This custom FasterXML Jackson {@code com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter} + * allows to append {@link VirtualProperty}-ies at the end of JSON output + * + * @see org.apache.logging.log4j.core.jackson.LogEventJacksonJsonMixIn} + * @see org.appenders.log4j2.elasticsearch.JacksonJsonLayout.Builder#withVirtualProperties(VirtualProperty...) + */ +public class VirtualPropertiesWriter extends VirtualBeanPropertyWriter { + + protected final VirtualProperty[] virtualProperties; + protected final ValueResolver valueResolver; + + VirtualPropertiesWriter() { + throw new UnsupportedOperationException(String.format( + "Invalid use of %s. Use virtualProperties based constructors", + VirtualPropertiesWriter.class.getSimpleName()) + ); + } + + public VirtualPropertiesWriter(VirtualProperty[] virtualProperties, ValueResolver valueResolver) { + this.virtualProperties = virtualProperties; + this.valueResolver = valueResolver; + } + + public VirtualPropertiesWriter( + BeanPropertyDefinition propDef, + Annotations annotations, + JavaType type, + VirtualProperty[] virtualProperties, + ValueResolver valueResolver + ) { + super(propDef, annotations, type); + this.virtualProperties = virtualProperties; + this.valueResolver = valueResolver; + } + + @Override + protected Object value(Object bean, JsonGenerator gen, SerializerProvider prov) { + throw new UnsupportedOperationException("Should not be used with this implementation. Use serializeAsField() to write value directly."); + } + + @Override + public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception { + if (virtualProperties.length > 0) { + for (int i = 0; i < virtualProperties.length; i++) { + gen.writeFieldName(virtualProperties[i].getName()); + gen.writeString(valueResolver.resolve(virtualProperties[i])); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public VirtualPropertiesWriter withConfig(MapperConfig config, AnnotatedClass declaringClass, BeanPropertyDefinition propDef, JavaType type) { + return new VirtualPropertiesWriter( + propDef, + new AnnotationCollector.OneAnnotation( + declaringClass.getRawType(), + declaringClass.getAnnotations().get(JsonAppend.class) + ), + type, + virtualProperties, + valueResolver + ); + } + + @Override + public void fixAccess(SerializationConfig config) { + // noop - fast path as super.getMember() returns null anyway + } + +} diff --git a/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/VirtualProperty.java b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/VirtualProperty.java new file mode 100644 index 00000000..4471afb6 --- /dev/null +++ b/log4j2-elasticsearch-core/src/main/java/org/appenders/log4j2/elasticsearch/VirtualProperty.java @@ -0,0 +1,127 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2019 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.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; + +/** + * Allows to define a property which will be appended to JSON output. + * Similar to Log4j2 KeyValuePair, value resolution is done with {@code org.apache.logging.log4j.core.lookup.StrSubstitutor}. + * Value may be static (resolved) or in a resolvable format defined by Log4j2 Lookups + */ +@Plugin(name = VirtualProperty.PLUGIN_NAME, category = Node.CATEGORY, printObject = true) +public class VirtualProperty { + + public static final String PLUGIN_NAME = "VirtualProperty"; + private final String name; + private String value; + private final boolean dynamic; + + /** + * @param name Name + * @param value May be static or in a resolvable format defined by Log4j2 Lookups + * @param isDynamic In case of resolvable properties, this flag indicates that resolved value may change over time + */ + public VirtualProperty(final String name, final String value, final boolean isDynamic) { + this.name = name; + this.value = value; + this.dynamic = isDynamic; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public void setValue(String resolved) { + this.value = resolved; + } + + /** + * @return if false, value SHOULD be resolved during initialization phase and SHOULD replaced using {@link #setValue(String)}, otherwise value SHOULD be resolved (and not replaced) on during serialization + * + * @see ValueResolver + */ + public boolean isDynamic() { + return dynamic; + } + + @Override + public String toString() { + return String.format("%s=%s", name, value); + } + + @PluginBuilderFactory + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder implements org.apache.logging.log4j.core.util.Builder { + + @PluginBuilderAttribute + private String name; + + @PluginBuilderAttribute + private String value; + + @PluginBuilderAttribute + private boolean dynamic; + + @Override + public VirtualProperty build() { + + if (name == null) { + throw new ConfigurationException("No name provided for " + PLUGIN_NAME); + } + + if (value == null) { + throw new ConfigurationException("No value provided for " + PLUGIN_NAME); + } + + return new VirtualProperty(name, value, dynamic); + + } + + public Builder withName(final String name) { + this.name = name; + return this; + } + + public Builder withValue(final String value) { + this.value = value; + return this; + } + + public Builder withDynamic(boolean isDynamic) { + this.dynamic = isDynamic; + return this; + } + + } + +} 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 9d5eb6f7..e0090235 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 @@ -25,6 +25,7 @@ import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.LifeCycle; import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.ConfigurationException; import org.apache.logging.log4j.core.filter.ThresholdFilter; import org.apache.logging.log4j.core.impl.DefaultLogEventFactory; @@ -34,7 +35,9 @@ import org.apache.logging.log4j.message.SimpleMessage; import org.appenders.log4j2.elasticsearch.ElasticsearchAppender.Builder; import org.appenders.log4j2.elasticsearch.mock.LifecycleTestHelper; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.powermock.api.mockito.PowerMockito; @@ -59,6 +62,9 @@ public class ElasticsearchAppenderTest { private static final String TEST_APPENDER_NAME = "testAppender"; + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Test public void builderReturnsNonNullObject() { @@ -72,39 +78,45 @@ public void builderReturnsNonNullObject() { assertNotNull(appender); } - @Test(expected = ConfigurationException.class) + @Test public void builderFailsWhenAppenderNameIsNull() { // given ElasticsearchAppender.Builder builder = createTestElasticsearchAppenderBuilder(); builder.withName(null); + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage("No name provided"); + // when builder.build(); } @Test - public void builderInitializesDefaultLayoutWhenLayoutIsNotProvided() throws IllegalAccessException { + public void builderFailsLayoutIsNotProvided() { // given ElasticsearchAppender.Builder builder = createTestElasticsearchAppenderBuilder(); builder.withLayout(null); - // when - ElasticsearchAppender appender = builder.build(); + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage("No layout provided"); - // then - assertNotNull(PowerMockito.field(ElasticsearchAppender.class, "layout").get(appender)); + // when + builder.build(); } - @Test(expected = ConfigurationException.class) + @Test public void builderFailsWhenBatchDeliveryIsNull() { // given ElasticsearchAppender.Builder builder = createTestElasticsearchAppenderBuilder(); builder.withBatchDelivery(null); + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage("No batchDelivery [AsyncBatchDelivery] provided"); + // when builder.build(); } @@ -347,7 +359,9 @@ public ItemAppender createInstance(boolean messageOnly, AbstractLayout layout, B } private LifeCycle createLifeCycleTestObject() { - return createTestElasticsearchAppenderBuilder(JacksonJsonLayout.newBuilder().build()).build(); + return createTestElasticsearchAppenderBuilder(JacksonJsonLayout.newBuilder() + .setConfiguration(LoggerContext.getContext(false).getConfiguration()) + .build()).build(); } private LogEvent createTestLogEvent() { @@ -386,7 +400,10 @@ protected ItemAppenderFactory createItemAppenderFactory() { } public static Builder createTestElasticsearchAppenderBuilder() { - return createTestElasticsearchAppenderBuilder(null); + return createTestElasticsearchAppenderBuilder(new JacksonJsonLayout.Builder() + .setConfiguration(LoggerContext.getContext(false).getConfiguration()) + .build() + ); } public static Builder createTestElasticsearchAppenderBuilder(AbstractLayout layout) { diff --git a/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonHandlerInstantiatorTest.java b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonHandlerInstantiatorTest.java new file mode 100644 index 00000000..6a3fd252 --- /dev/null +++ b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/JacksonHandlerInstantiatorTest.java @@ -0,0 +1,164 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2019 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.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; +import com.fasterxml.jackson.databind.ser.VirtualBeanPropertyWriter; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class JacksonHandlerInstantiatorTest { + + @Test + public void startsWithNoErrors() { + + // given + VirtualProperty[] customProperties = new VirtualProperty[0]; + ValueResolver valueResolver = Mockito.mock(ValueResolver.class); + + // when + JacksonHandlerInstantiator result = createTestHandlerInstantiator(customProperties, valueResolver); + + // then + Assert.assertNotNull(result); + } + + @Test + public void virtualPropertyWriterInstanceReturnsSingleton() { + + // given + VirtualProperty[] customProperties = new VirtualProperty[0]; + Log4j2Lookup valueResolver = new Log4j2Lookup(null); + JacksonHandlerInstantiator handlerInstantiator = createTestHandlerInstantiator(customProperties, valueResolver); + + MapperConfig config = new ObjectMapper().getSerializationConfig(); + + // when + VirtualBeanPropertyWriter result1 = handlerInstantiator.virtualPropertyWriterInstance( + config, + VirtualPropertiesWriter.class + ); + VirtualBeanPropertyWriter result2 = handlerInstantiator.virtualPropertyWriterInstance( + config, + VirtualPropertiesWriter.class + ); + + // then + Assert.assertTrue(result1 == result2); + + } + + @Test + public void deserializerInstanceReturnsNull() { + + // given + VirtualProperty[] customProperties = new VirtualProperty[0]; + Log4j2Lookup valueResolver = new Log4j2Lookup(null); + JacksonHandlerInstantiator handlerInstantiator = createTestHandlerInstantiator(customProperties, valueResolver); + + // when + JsonDeserializer result = handlerInstantiator.deserializerInstance(null, null, null); + + // then + Assert.assertNull(result); + + } + + @Test + public void keyDeserializerInstanceReturnsNull() { + + // given + VirtualProperty[] customProperties = new VirtualProperty[0]; + Log4j2Lookup valueResolver = new Log4j2Lookup(null); + JacksonHandlerInstantiator handlerInstantiator = createTestHandlerInstantiator(customProperties, valueResolver); + + // when + KeyDeserializer result = handlerInstantiator.keyDeserializerInstance(null, null, null); + + // then + Assert.assertNull(result); + + } + + @Test + public void serializerInstanceReturnsNull() { + + // given + VirtualProperty[] customProperties = new VirtualProperty[0]; + Log4j2Lookup valueResolver = new Log4j2Lookup(null); + JacksonHandlerInstantiator handlerInstantiator = createTestHandlerInstantiator(customProperties, valueResolver); + + // when + JsonSerializer result = handlerInstantiator.serializerInstance(null, null, null); + + // then + Assert.assertNull(result); + + } + + + @Test + public void typeResolverBuilderInstanceReturnsNull() { + + // given + VirtualProperty[] customProperties = new VirtualProperty[0]; + Log4j2Lookup valueResolver = new Log4j2Lookup(null); + JacksonHandlerInstantiator handlerInstantiator = createTestHandlerInstantiator(customProperties, valueResolver); + + // when + TypeResolverBuilder result = handlerInstantiator.typeResolverBuilderInstance(null, null, null); + + // then + Assert.assertNull(result); + + } + + @Test + public void typeIdResolverInstanceReturnsNull() { + + // given + VirtualProperty[] customProperties = new VirtualProperty[0]; + Log4j2Lookup valueResolver = new Log4j2Lookup(null); + JacksonHandlerInstantiator handlerInstantiator = createTestHandlerInstantiator(customProperties, valueResolver); + + // when + TypeIdResolver result = handlerInstantiator.typeIdResolverInstance(null, null, null); + + // then + Assert.assertNull(result); + + } + + private JacksonHandlerInstantiator createTestHandlerInstantiator(VirtualProperty[] customProperties, ValueResolver valueResolver) { + return new JacksonHandlerInstantiator( + customProperties, + valueResolver + ); + } + +} 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 index 6dbd6ad0..91a827ff 100644 --- 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 @@ -24,8 +24,12 @@ import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; import com.fasterxml.jackson.module.afterburner.AfterburnerModule; import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.ConfigurationException; import org.apache.logging.log4j.core.impl.Log4jLogEvent; import org.apache.logging.log4j.core.jackson.ExtendedLog4j2JsonModule; import org.apache.logging.log4j.core.jackson.LogEventJacksonJsonMixIn; @@ -37,19 +41,21 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; import java.util.ArrayList; +import java.util.UUID; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -99,6 +105,21 @@ public void throwsOnByteArrayCreationAttempt() { } + @Test + public void throwsOnNullConfiguration() { + + // given + JacksonJsonLayout.Builder builder = createDefaultTestBuilder() + .setConfiguration(null); + + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage("No Configuration instance provided for " + JacksonJsonLayout.PLUGIN_NAME); + + // when + builder.build(); + + } + @Test public void builderBuildsLayoutWithDefaultItemSourceFactoryIfNotConfigured() { @@ -142,7 +163,7 @@ public void builderBuildsMapperWithAfterburnerIfConfigured() { JacksonJsonLayout.Builder builder = spy(createDefaultTestBuilder()); builder.withAfterburner(true); - ObjectMapper objectMapper = Mockito.mock(ObjectMapper.class); + ObjectMapper objectMapper = spy(new ObjectMapper()); when(builder.createDefaultObjectMapper()).thenReturn(objectMapper); // when @@ -162,7 +183,8 @@ public void builderBuildsMapperWithMixInsIfConfigured() { JacksonJsonLayout.Builder builder = spy(createDefaultTestBuilder()); builder.withMixins(JacksonMixInTest.createDefaultTestBuilder().build()); - ObjectMapper objectMapper = Mockito.mock(ObjectMapper.class); + ObjectMapper objectMapper = spy(ObjectMapper.class); + when(builder.createDefaultObjectMapper()).thenReturn(objectMapper); // when @@ -178,6 +200,82 @@ public void builderBuildsMapperWithMixInsIfConfigured() { } + @Test + public void builderBuildsMapperWithCustomHandlerInstantiator() { + + // given + JacksonJsonLayout.Builder builder = spy(createDefaultTestBuilder()); + + ObjectMapper objectMapper = spy(ObjectMapper.class); + + when(builder.createDefaultObjectMapper()).thenReturn(objectMapper); + + // when + builder.build(); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(SerializationConfig.class); + verify(objectMapper).setConfig(captor.capture()); + + HandlerInstantiator handlerInstantiator = captor.getValue().getHandlerInstantiator(); + assertTrue(handlerInstantiator instanceof JacksonHandlerInstantiator); + + } + + @Test + public void builderResolvesNonDynamicVirtualProperties() { + + // given + JacksonJsonLayout.Builder builder = spy(createDefaultTestBuilder()); + + ValueResolver valueResolver = mock(ValueResolver.class); + when(builder.createValueResolver()).thenReturn(valueResolver); + + String expectedValue = UUID.randomUUID().toString(); + VirtualProperty virtualProperty = new VirtualProperty.Builder() + .withDynamic(false) + .withName(UUID.randomUUID().toString()) + .withValue(expectedValue) + .build(); + + builder.withVirtualProperties(virtualProperty); + + // when + builder.build(); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(valueResolver).resolve(captor.capture()); + + assertEquals(expectedValue, captor.getValue()); + } + + @Test + public void builderDoesNotResolveDynamicVirtualProperties() { + + // given + JacksonJsonLayout.Builder builder = spy(createDefaultTestBuilder()); + + ValueResolver valueResolver = mock(ValueResolver.class); + when(builder.createValueResolver()).thenReturn(valueResolver); + + String expectedValue = UUID.randomUUID().toString(); + VirtualProperty virtualProperty = new VirtualProperty.Builder() + .withDynamic(true) + .withName(UUID.randomUUID().toString()) + .withValue(expectedValue) + .build(); + + builder.withVirtualProperties(virtualProperty); + + // when + builder.build(); + + // then + verify(valueResolver, never()).resolve(anyString()); + + } + @Test public void builderCreatesExtendedObjectWriter() { @@ -205,8 +303,23 @@ public void builderConfiguresExtendedObjectWriter() { } + @Test + public void builderCreatesLog4j2ValueResolver() { + + // given + JacksonJsonLayout.Builder builder = spy(createDefaultTestBuilder()); + + // when + ValueResolver result = builder.createValueResolver(); + + // then + assertTrue(result instanceof Log4j2Lookup); + + } + private JacksonJsonLayout.Builder createDefaultTestBuilder() { - return JacksonJsonLayout.newBuilder(); + return JacksonJsonLayout.newBuilder() + .setConfiguration(LoggerContext.getContext(false).getConfiguration()); } @Test diff --git a/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/Log4j2LookupTest.java b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/Log4j2LookupTest.java new file mode 100644 index 00000000..4142a4df --- /dev/null +++ b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/Log4j2LookupTest.java @@ -0,0 +1,131 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2019 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.LoggerContext; +import org.apache.logging.log4j.core.lookup.StrSubstitutor; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class Log4j2LookupTest { + + @Test + public void startsSuccessfully() { + + // given + StrSubstitutor strSubstitutor = createDefaultTestStrSubstitutor(); + + // when + Log4j2Lookup result = createDefaultTestLog4j2Lookup(strSubstitutor); + + // then + Assert.assertNotNull(result); + + } + + @Test + public void resolvesDynamicPropertyOnEachCall() { + + // given + String expectedValue = UUID.randomUUID().toString(); + VirtualProperty virtualProperty = VirtualPropertyTest.createDefaultVirtualPropertyBuilder() + .withValue(expectedValue) + .withDynamic(true) + .build(); + + StrSubstitutor strSubstitutor = spy(createDefaultTestStrSubstitutor()); + Log4j2Lookup lookup = createDefaultTestLog4j2Lookup(strSubstitutor); + + // when + lookup.resolve(virtualProperty); + lookup.resolve(virtualProperty); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + + verify(strSubstitutor, times(2)).replace(captor.capture()); + + Assert.assertEquals(expectedValue, captor.getAllValues().get(0)); + Assert.assertEquals(expectedValue, captor.getAllValues().get(1)); + + } + + @Test + public void resolvesDynamicPropertyDelegatesToStringBasedApi() { + + // given + String expectedValue = UUID.randomUUID().toString(); + VirtualProperty virtualProperty = VirtualPropertyTest.createDefaultVirtualPropertyBuilder() + .withValue(expectedValue) + .withDynamic(true) + .build(); + + Log4j2Lookup lookup = spy(createDefaultTestLog4j2Lookup(createDefaultTestStrSubstitutor())); + + // when + lookup.resolve(virtualProperty); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + + verify(lookup, times(1)).resolve(captor.capture()); + + Assert.assertEquals(expectedValue, captor.getValue()); + + } + + @Test + public void doesNotResolveNonDynamicVirtualProperties() { + + // given + VirtualProperty virtualProperty = VirtualPropertyTest.createDefaultVirtualPropertyBuilder() + .withDynamic(false) + .build(); + + StrSubstitutor strSubstitutor = spy(createDefaultTestStrSubstitutor()); + Log4j2Lookup lookup = createDefaultTestLog4j2Lookup(strSubstitutor); + + // when + lookup.resolve(virtualProperty); + + // then + verify(strSubstitutor, never()).replace(anyString()); + + } + + private Log4j2Lookup createDefaultTestLog4j2Lookup(StrSubstitutor strSubstitutor) { + return new Log4j2Lookup(strSubstitutor); + } + + private StrSubstitutor createDefaultTestStrSubstitutor() { + return LoggerContext.getContext(false).getConfiguration().getStrSubstitutor(); + } + +} diff --git a/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/VirtualPropertiesWriterTest.java b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/VirtualPropertiesWriterTest.java new file mode 100644 index 00000000..7bb9e33b --- /dev/null +++ b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/VirtualPropertiesWriterTest.java @@ -0,0 +1,236 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2019 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.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.introspect.AnnotatedClass; +import com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver; +import com.fasterxml.jackson.databind.introspect.VirtualAnnotatedMember; +import com.fasterxml.jackson.databind.util.SimpleBeanPropertyDefinition; +import org.apache.logging.log4j.core.LogEvent; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.UUID; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class VirtualPropertiesWriterTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void defaultConstructorIsNotSupported() { + + // given + expectedException.expect(UnsupportedOperationException.class); + expectedException.expectMessage("Invalid use of " + VirtualPropertiesWriter.class.getSimpleName()); + + // when + new VirtualPropertiesWriter(); + } + + @Test + public void startsSuccessfully() { + + // when + VirtualPropertiesWriter result = new VirtualPropertiesWriter( + new VirtualProperty[0], + mock(ValueResolver.class) + ); + + // then + assertNotNull(result); + + } + + @Test + public void overridenValueIsNotSupported() { + + // given + VirtualPropertiesWriter writer = new VirtualPropertiesWriter( + new VirtualProperty[0], + mock(ValueResolver.class) + ); + + expectedException.expect(UnsupportedOperationException.class); + expectedException.expectMessage("Should not be used with this implementation"); + + // when + writer.value(null, null, null); + + } + + @Test + public void overridenFixAccessIsNoop() { + + // given + ObjectMapper objectMapper = new ObjectMapper(); + SerializationConfig config = objectMapper.getSerializationConfig(); + + VirtualPropertiesWriter writer = spy(new VirtualPropertiesWriter( + new VirtualProperty[0], + mock(ValueResolver.class) + )); + + // when + writer.fixAccess(config); + + // then + verify(writer, never()).getMember(); + + } + + @Test + public void withConfigReturnsConfiguredWriter() { + + // given + ObjectMapper objectMapper = new ObjectMapper(); + SerializationConfig config = objectMapper.getSerializationConfig(); + + VirtualPropertiesWriter writer = spy(new VirtualPropertiesWriter( + new VirtualProperty[0], + mock(ValueResolver.class) + )); + + JavaType javaType = config.constructType(LogEvent.class); + AnnotatedClass annotatedClass = AnnotatedClassResolver.resolve( + config, + javaType, + null + ); + + SimpleBeanPropertyDefinition simpleBeanPropertyDefinition = SimpleBeanPropertyDefinition.construct( + config, + new VirtualAnnotatedMember( + annotatedClass, + LogEvent.class, + "virtualProperties", + javaType + ) + ); + + VirtualPropertiesWriter result = writer.withConfig( + config, + annotatedClass, + simpleBeanPropertyDefinition, + config.constructType(VirtualProperty.class) + ); + + // then + assertArrayEquals(writer.virtualProperties, result.virtualProperties); + assertEquals(writer.valueResolver, result.valueResolver); + + } + + @Test + public void serializeAsFieldResolvesVirtualPropertyValue() throws Exception { + + // given + String expectedValue = UUID.randomUUID().toString(); + VirtualProperty virtualProperty = spy(VirtualPropertyTest.createDefaultVirtualPropertyBuilder() + .withValue(expectedValue) + .withDynamic(false) + .build() + ); + + ValueResolver valueResolver = mock(ValueResolver.class); + + VirtualPropertiesWriter writer = new VirtualPropertiesWriter( + new VirtualProperty[] { virtualProperty }, + valueResolver + ); + + // when + writer.serializeAsField(new Object(), mock(JsonGenerator.class), mock(SerializerProvider.class)); + + // then + verify(valueResolver).resolve(eq(virtualProperty)); + + } + + @Test + public void serializeAsFieldWritesGivenProperties() throws Exception { + + // given + String expectedName = UUID.randomUUID().toString(); + String expectedValue = UUID.randomUUID().toString(); + VirtualProperty virtualProperty = VirtualPropertyTest.createDefaultVirtualPropertyBuilder() + .withName(expectedName) + .withValue(expectedValue) + .build(); + + ValueResolver valueResolver = mock(ValueResolver.class); + when(valueResolver.resolve((VirtualProperty) any())).thenReturn(expectedValue); + + VirtualPropertiesWriter writer = new VirtualPropertiesWriter( + new VirtualProperty[] { virtualProperty }, + valueResolver + ); + + JsonGenerator jsonGenerator = mock(JsonGenerator.class); + + // when + writer.serializeAsField(new Object(), jsonGenerator, mock(SerializerProvider.class)); + + // then + verify(jsonGenerator).writeFieldName(eq(expectedName)); + verify(jsonGenerator).writeString(eq(expectedValue)); + + } + + @Test + public void serializeAsFieldDoesNotWritePropertiesIfNoPropertiesProvided() throws Exception { + + // given + VirtualPropertiesWriter writer = new VirtualPropertiesWriter( + new VirtualProperty[0], + mock(ValueResolver.class) + ); + + JsonGenerator jsonGenerator = mock(JsonGenerator.class); + + // when + writer.serializeAsField(new Object(), jsonGenerator, mock(SerializerProvider.class)); + + // then + verify(jsonGenerator, never()).writeFieldName(anyString()); + verify(jsonGenerator, never()).writeString(anyString()); + + } + +} diff --git a/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/VirtualPropertyTest.java b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/VirtualPropertyTest.java new file mode 100644 index 00000000..9dc0e1fe --- /dev/null +++ b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/VirtualPropertyTest.java @@ -0,0 +1,159 @@ +package org.appenders.log4j2.elasticsearch; + +/*- + * #%L + * log4j2-elasticsearch + * %% + * Copyright (C) 2019 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.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class VirtualPropertyTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void builderFailsWhenNameIsNull() { + + // given + VirtualProperty.Builder builder = createDefaultVirtualPropertyBuilder() + .withName(null); + + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage("No name provided for " + VirtualProperty.PLUGIN_NAME); + + // then + builder.build(); + + } + + @Test + public void builderFailsWhenValueIsNull() { + + // given + VirtualProperty.Builder builder = createDefaultVirtualPropertyBuilder() + .withValue(null); + + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage("No value provided for " + VirtualProperty.PLUGIN_NAME); + + // then + builder.build(); + } + + @Test + public void builderSetsName() { + + // given + String expectedName = UUID.randomUUID().toString(); + VirtualProperty.Builder builder = createDefaultVirtualPropertyBuilder() + .withName(expectedName); + + // when + VirtualProperty property = builder.build(); + + // then + assertEquals(expectedName, property.getName()); + + } + + @Test + public void builderSetsValue() { + + // given + String expectedName = UUID.randomUUID().toString(); + VirtualProperty.Builder builder = createDefaultVirtualPropertyBuilder() + .withName(expectedName); + + // when + VirtualProperty property = builder.build(); + + // then + assertEquals(expectedName, property.getName()); + + } + + @Test + public void builderSetsDynamic() { + + // given + VirtualProperty.Builder builder = createDefaultVirtualPropertyBuilder() + .withDynamic(true); + + // when + VirtualProperty property = builder.build(); + + // then + assertTrue(property.isDynamic()); + + } + + @Test + public void valueCanBeOverridenAfterCreation() { + + // given + VirtualProperty property = createDefaultVirtualPropertyBuilder().build(); + String expectedValue = UUID.randomUUID().toString(); + + assertNotEquals(expectedValue, property.getValue()); + + // when + property.setValue(expectedValue); + + // then + assertEquals(expectedValue, property.getValue()); + + } + + @Test + public void toStringPrintsFormattedInfo() { + + // given + String expectedName = UUID.randomUUID().toString(); + String expectedValue = UUID.randomUUID().toString(); + + VirtualProperty property = createDefaultVirtualPropertyBuilder() + .withName(expectedName) + .withValue(expectedValue) + .build(); + + // when + String result = property.toString(); + + // then + assertEquals(result, String.format("%s=%s", expectedName, expectedValue)); + + } + + public static VirtualProperty.Builder createDefaultVirtualPropertyBuilder() { + return VirtualProperty.newBuilder() + .withName(UUID.randomUUID().toString()) + .withValue(UUID.randomUUID().toString()) + .withDynamic(false); + } + +} diff --git a/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/smoke/SmokeTestBase.java b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/smoke/SmokeTestBase.java index f62e14cb..a2dd8fce 100644 --- a/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/smoke/SmokeTestBase.java +++ b/log4j2-elasticsearch-core/src/test/java/org/appenders/log4j2/elasticsearch/smoke/SmokeTestBase.java @@ -53,13 +53,13 @@ public abstract class SmokeTestBase { private final AtomicInteger localCounter = new AtomicInteger(); // TODO: expose all via system properties - public static final int LIMIT_PER_SEC = 20000; + public static final int LIMIT_PER_SEC = 10000; public static final int INITIAL_SLEEP_PER_THREAD = 10; public static final int MILLIS_BEFORE_SHUTDOWN = 30000; public static final int MILLIS_AFTER_SHUTDOWN = 3000000; public static final int NUMBER_OF_PRODUCERS = 100; public static final int LOG_SIZE = 1; - private boolean secure = true; + private boolean secure = false; private final AtomicInteger numberOfLogs = new AtomicInteger(1000000); public abstract ElasticsearchAppender.Builder createElasticsearchAppenderBuilder(boolean messageOnly, boolean buffered, boolean secured); @@ -77,7 +77,7 @@ protected String createLog() { public final void createLoggerProgrammatically(ElasticsearchAppender.Builder appenderBuilder, Function delegateSupplier) { - AsyncLoggerContext ctx = (AsyncLoggerContext) LoggerContext.getContext(false); + LoggerContext ctx = LoggerContext.getContext(false); final Configuration config = ctx.getConfiguration();