diff --git a/README.md b/README.md index 633c0cf..82bf55e 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ In your `logback.xml`: false 100 + false host @@ -110,6 +111,7 @@ Configuration Reference * `includeMdc` (optional, default false): If set to `true`, then all [MDC](http://www.slf4j.org/api/org/slf4j/MDC.html) values will be mapped to properties on the JSON payload. * `maxMessageSize` (optional, default -1): If set to a number greater than 0, truncate messages larger than this length, then append "`..`" to denote that the message was truncated * `authentication` (optional): Add the ability to send authentication headers (see below) + * `enableContextMap` (optional): If the latest parameter in logger call is of type java.util.Map then all content of it will be traversed and written with prefix `context.*`. For event-specific custom fields. The fields `@timestamp` and `message` are always sent and can not currently be configured. Additional fields can be sent by adding `` elements to the `` set. @@ -154,3 +156,17 @@ Included is also an Elasticsearch appender for Logback Access. The configuration * The Appender class name is `com.internetitem.logback.elasticsearch.ElasticsearchAccessAppender` * The `value` for each `property` uses the [Logback Access conversion words](http://logback.qos.ch/manual/layouts.html#logback-access). + +Event-specific custom fields +============================ +Log line: + + log.info("Service started in {} seconds", duration/1000, Collections.singletonMap("duration", duration)); + +Result: + + { + "@timestamp": "2014-06-04T15:26:14.464+02:00", + "message": "Service started in 12 seconds", + "duration": 12368, + } \ No newline at end of file diff --git a/src/main/java/com/internetitem/logback/elasticsearch/AbstractElasticsearchAppender.java b/src/main/java/com/internetitem/logback/elasticsearch/AbstractElasticsearchAppender.java index a1e48ea..f13c765 100644 --- a/src/main/java/com/internetitem/logback/elasticsearch/AbstractElasticsearchAppender.java +++ b/src/main/java/com/internetitem/logback/elasticsearch/AbstractElasticsearchAppender.java @@ -133,4 +133,8 @@ public void setAuthentication(Authentication auth) { public void setMaxMessageSize(int maxMessageSize) { settings.setMaxMessageSize(maxMessageSize); } + + public void setEnableContextMap(boolean enableContextMap) { + settings.setEnableContextMap(enableContextMap); + } } diff --git a/src/main/java/com/internetitem/logback/elasticsearch/ClassicElasticsearchPublisher.java b/src/main/java/com/internetitem/logback/elasticsearch/ClassicElasticsearchPublisher.java index 3a0a08e..f34af58 100644 --- a/src/main/java/com/internetitem/logback/elasticsearch/ClassicElasticsearchPublisher.java +++ b/src/main/java/com/internetitem/logback/elasticsearch/ClassicElasticsearchPublisher.java @@ -9,6 +9,7 @@ import com.internetitem.logback.elasticsearch.config.Settings; import com.internetitem.logback.elasticsearch.util.AbstractPropertyAndEncoder; import com.internetitem.logback.elasticsearch.util.ClassicPropertyAndEncoder; +import com.internetitem.logback.elasticsearch.util.ContextMapWriter; import com.internetitem.logback.elasticsearch.util.ErrorReporter; import java.io.IOException; @@ -16,8 +17,11 @@ public class ClassicElasticsearchPublisher extends AbstractElasticsearchPublisher { + protected ContextMapWriter contextMapWriter; + public ClassicElasticsearchPublisher(Context context, ErrorReporter errorReporter, Settings settings, ElasticsearchProperties properties, HttpRequestHeaders headers) throws IOException { super(context, errorReporter, settings, properties, headers); + contextMapWriter = new ContextMapWriter(); } @Override @@ -49,5 +53,9 @@ protected void serializeCommonFields(JsonGenerator gen, ILoggingEvent event) thr gen.writeObjectField(entry.getKey(), entry.getValue()); } } + + if (settings.isEnableContextMap()) { + contextMapWriter.writeContextMap(gen, event); + } } } diff --git a/src/main/java/com/internetitem/logback/elasticsearch/config/Settings.java b/src/main/java/com/internetitem/logback/elasticsearch/config/Settings.java index 983ba06..61c2e20 100644 --- a/src/main/java/com/internetitem/logback/elasticsearch/config/Settings.java +++ b/src/main/java/com/internetitem/logback/elasticsearch/config/Settings.java @@ -23,6 +23,7 @@ public class Settings { private int maxQueueSize = 100 * 1024 * 1024; private Authentication authentication; private int maxMessageSize = -1; + private boolean enableContextMap; public String getIndex() { return index; @@ -162,4 +163,12 @@ public int getMaxMessageSize() { public void setMaxMessageSize(int maxMessageSize) { this.maxMessageSize = maxMessageSize; } + + public boolean isEnableContextMap() { + return enableContextMap; + } + + public void setEnableContextMap(boolean enableContextMap) { + this.enableContextMap = enableContextMap; + } } diff --git a/src/main/java/com/internetitem/logback/elasticsearch/util/ContextMapWriter.java b/src/main/java/com/internetitem/logback/elasticsearch/util/ContextMapWriter.java new file mode 100644 index 0000000..5e555e7 --- /dev/null +++ b/src/main/java/com/internetitem/logback/elasticsearch/util/ContextMapWriter.java @@ -0,0 +1,62 @@ +package com.internetitem.logback.elasticsearch.util; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.fasterxml.jackson.core.JsonGenerator; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +public class ContextMapWriter { + + public void writeContextMap(JsonGenerator gen, ILoggingEvent event) throws IOException { + Object[] arguments = event.getArgumentArray(); + if (arguments == null || arguments.length == 0) { + return; + } + Object lastElement = arguments[arguments.length - 1]; + if (lastElement instanceof Map) { + Map indexes = traverseMap(new HashMap<>(), "context", (Map)lastElement); + for (Map.Entry entry : indexes.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + gen.writeObjectField(key, value); + } + } + } + + static void traverseObject(Map accumulator, String context, Object object) { + if (object == null) { + return; + } + if (object instanceof Map) { + traverseMap(accumulator, context, (Map) object); + } else if (object instanceof Collection) { + traverseCollection(accumulator, context, (Collection) object); + } else if (object instanceof Number) { + accumulator.put(context, object); + } else { + accumulator.put(context, Objects.toString(object)); + } + } + + static void traverseCollection(Map accumulator, String context, Collection object) { + Iterator iterator = object.iterator(); + int i = 0; + while (iterator.hasNext()) { + Object v = iterator.next(); + traverseObject(accumulator, context + "." + i, v); + i++; + } + } + + static Map traverseMap(Map accumulator, String context, Map object) { + for (Map.Entry entry : object.entrySet()) { + traverseObject(accumulator, context + "." + entry.getKey(), entry.getValue()); + } + return accumulator; + } +} \ No newline at end of file diff --git a/src/test/java/com/internetitem/logback/elasticsearch/ElasticsearchAppenderTest.java b/src/test/java/com/internetitem/logback/elasticsearch/ElasticsearchAppenderTest.java index 4609270..cb15521 100644 --- a/src/test/java/com/internetitem/logback/elasticsearch/ElasticsearchAppenderTest.java +++ b/src/test/java/com/internetitem/logback/elasticsearch/ElasticsearchAppenderTest.java @@ -189,6 +189,7 @@ public void should_delegate_setters_to_settings() throws MalformedURLException { int aSleepTime = 10000; int readTimeout = 10000; int connectTimeout = 5000; + boolean enableContextMap = true; appender.setIncludeCallerData(includeCallerData); appender.setSleepTime(aSleepTime); @@ -205,6 +206,7 @@ public void should_delegate_setters_to_settings() throws MalformedURLException { appender.setConnectTimeout(connectTimeout); appender.setRawJsonMessage(rawJsonMessage); appender.setIncludeMdc(includeMdc); + appender.setEnableContextMap(enableContextMap); verify(settings, times(1)).setReadTimeout(readTimeout); verify(settings, times(1)).setSleepTime(aSleepTime); @@ -221,6 +223,7 @@ public void should_delegate_setters_to_settings() throws MalformedURLException { verify(settings, times(1)).setConnectTimeout(connectTimeout); verify(settings, times(1)).setRawJsonMessage(rawJsonMessage); verify(settings, times(1)).setIncludeMdc(includeMdc); + verify(settings, times(1)).setEnableContextMap(enableContextMap); } diff --git a/src/test/java/com/internetitem/logback/elasticsearch/util/ContextMapWriterTest.java b/src/test/java/com/internetitem/logback/elasticsearch/util/ContextMapWriterTest.java new file mode 100644 index 0000000..74b59c9 --- /dev/null +++ b/src/test/java/com/internetitem/logback/elasticsearch/util/ContextMapWriterTest.java @@ -0,0 +1,57 @@ +package com.internetitem.logback.elasticsearch.util; + +import ch.qos.logback.classic.spi.LoggingEvent; +import com.fasterxml.jackson.core.JsonGenerator; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; + +import java.io.IOException; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +@RunWith(MockitoJUnitRunner.class) +public class ContextMapWriterTest { + + @Mock + private JsonGenerator jsonGenerator; + + + private ContextMapWriter contextMapWriter; + + @Before + public void setup() throws IOException { + contextMapWriter = new ContextMapWriter(); + } + + @Test + public void should_write_if_last_element_is_map() throws IOException { + LoggingEvent event = new LoggingEvent(); + event.setArgumentArray(new Object[] {"123", ImmutableMap.of("test", 123, "test2", "foo")}); + contextMapWriter.writeContextMap(jsonGenerator, event); + verify(jsonGenerator, times(1)).writeObjectField("context.test", 123); + verify(jsonGenerator, times(1)).writeObjectField("context.test2", "foo"); + } + + @Test + public void should_not_write_if_arguments_null_or_empty() throws IOException { + LoggingEvent event = new LoggingEvent(); + contextMapWriter.writeContextMap(jsonGenerator, event); + event.setArgumentArray(new Object[]{}); + contextMapWriter.writeContextMap(jsonGenerator, event); + verifyZeroInteractions(jsonGenerator); + } + + @Test + public void should_not_write_if_last_element_not_map() throws IOException { + LoggingEvent event = new LoggingEvent(); + event.setArgumentArray(new Object[]{"23", 3243}); + contextMapWriter.writeContextMap(jsonGenerator, event); + verifyZeroInteractions(jsonGenerator); + } +} \ No newline at end of file