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();