diff --git a/README.md b/README.md index 9a66e1ef..8d1a62bb 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ The structure of the output, and the data it contains, is fully configurable. * [LoggingEvent Fields](#loggingevent-fields) * [Standard Fields](#standard-fields) * [MDC fields](#mdc-fields) + * [Key Value Pair fields](#key-value-pair-fields) * [Context fields](#context-fields) * [Caller Info Fields](#caller-info-fields) * [Custom Fields](#custom-fields) @@ -990,11 +991,12 @@ The field names can be customized (see [Customizing Standard Field Names](#custo ### MDC fields -By default, each entry in the Mapped Diagnostic Context (MDC) (`org.slf4j.MDC`) -will appear as a field in the LoggingEvent. +By default, `LogstashEncoder`/`LogstashLayout` will write each +[Mapped Diagnostic Context (MDC) (`org.slf4j.MDC`)](https://www.slf4j.org/api/org/slf4j/MDC.html) +entry to the output. -This can be fully disabled by specifying `false`, -in the encoder/layout/appender configuration. +To disable writing MDC entries, add `false` +to the `LogstashEncoder`/`LogstashLayout` configuration. You can also configure specific entries in the MDC to be included or excluded as follows: @@ -1020,7 +1022,7 @@ It is a configuration error to specify both included and excluded key names. By default, the MDC key is used as the field name in the output. To use an alternative field name in the output for an MDC entry, -specify`mdcKeyName=fieldName`: +specify `mdcKeyName=fieldName`: ```xml @@ -1028,6 +1030,47 @@ specify`mdcKeyName=fieldName`: ``` +### Key Value Pair Fields + +Slf4j 2's [fluent API](https://www.slf4j.org/manual.html#fluent) supports attaching key value pairs to the log event. + +`LogstashEncoder`/`LogstashLayout` will write each key value pair as a field in the output by default. + +To disable writing key value pairs, add `false` +to the `LogstashEncoder`/`LogstashLayout` configuration. + +You can also configure specific key value pairs to be included or excluded as follows: + +```xml + + key1ToInclude + key2ToInclude + +``` + +or + +```xml + + key1ToExclude + key2ToExclude + +``` + +When key names are specified for inclusion, then all other keys will be excluded. +When key names are specified for exclusion, then all other keys will be included. +It is a configuration error to specify both included and excluded key names. + +By default, the key is used as the field name in the output. +To use an alternative field name in the output for an key value pair, +specify`keyName=fieldName`: + +```xml + + key1=alternateFieldNameForKey1 + +``` + ### Context fields By default, each property of Logback's Context (`ch.qos.logback.core.Context`) @@ -2593,6 +2636,24 @@ The provider name is the xml element name to use when configuring. Each provider + + keyValuePairs + +

Outputs key value pairs added via slf4j's fluent api. + Will include all key value pairs by default. + When key names are specified for inclusion, then all other keys will be excluded. + When key names are specified for exclusion, then all other keys will be included. + It is a configuration error to specify both included and excluded key names. +

+ + + message

Formatted log event message.

diff --git a/src/main/java/net/logstash/logback/LogstashFormatter.java b/src/main/java/net/logstash/logback/LogstashFormatter.java index 78a8fa42..987e8c44 100644 --- a/src/main/java/net/logstash/logback/LogstashFormatter.java +++ b/src/main/java/net/logstash/logback/LogstashFormatter.java @@ -26,6 +26,7 @@ import net.logstash.logback.composite.LogstashVersionJsonProvider; import net.logstash.logback.composite.loggingevent.ArgumentsJsonProvider; import net.logstash.logback.composite.loggingevent.CallerDataJsonProvider; +import net.logstash.logback.composite.loggingevent.KeyValuePairsJsonProvider; import net.logstash.logback.composite.loggingevent.LogLevelJsonProvider; import net.logstash.logback.composite.loggingevent.LogLevelValueJsonProvider; import net.logstash.logback.composite.loggingevent.LoggerNameJsonProvider; @@ -45,6 +46,7 @@ import ch.qos.logback.core.spi.ContextAware; import com.fasterxml.jackson.databind.JsonNode; import org.slf4j.MDC; +import org.slf4j.event.KeyValuePair; /** * A {@link LoggingEventCompositeJsonFormatter} that contains a common @@ -87,6 +89,11 @@ public class LogstashFormatter extends LoggingEventCompositeJsonFormatter { * to the logic in {@link MdcJsonProvider}. */ private MdcJsonProvider mdcProvider = new MdcJsonProvider(); + /** + * When not null, {@link KeyValuePair} properties will be included according + * to the logic in {@link KeyValuePairsJsonProvider}. + */ + private KeyValuePairsJsonProvider keyValuePairsProvider = new KeyValuePairsJsonProvider(); private GlobalCustomFieldsJsonProvider globalCustomFieldsProvider; /** * When not null, markers will be included according @@ -123,6 +130,7 @@ public LogstashFormatter(ContextAware declaredOrigin, boolean includeCallerData, getProviders().addStackTrace(this.stackTraceProvider); getProviders().addContext(this.contextProvider); getProviders().addMdc(this.mdcProvider); + getProviders().addKeyValuePairs(this.keyValuePairsProvider); getProviders().addGlobalCustomFields(this.globalCustomFieldsProvider); getProviders().addTags(this.tagsProvider); getProviders().addLogstashMarkers(this.logstashMarkersProvider); @@ -222,6 +230,22 @@ public void setIncludeMdc(boolean includeMdc) { } } + public boolean isIncludeKeyValuePairs() { + return this.keyValuePairsProvider != null; + } + + public void setIncludeKeyValuePairs(boolean includeKeyValuePairs) { + if (isIncludeKeyValuePairs() != includeKeyValuePairs) { + if (includeKeyValuePairs) { + keyValuePairsProvider = new KeyValuePairsJsonProvider(); + addProvider(keyValuePairsProvider); + } else { + getProviders().removeProvider(keyValuePairsProvider); + keyValuePairsProvider = null; + } + } + } + public boolean isIncludeTags() { return this.tagsProvider != null; } @@ -299,6 +323,44 @@ public void addMdcKeyFieldName(String mdcKeyFieldName) { } } + public List getIncludeKeyValueKeyNames() { + return isIncludeKeyValuePairs() + ? keyValuePairsProvider.getIncludeKeyNames() + : Collections.emptyList(); + } + + public void addIncludeKeyValueKeyName(String includedKeyValueKeyName) { + if (isIncludeKeyValuePairs()) { + keyValuePairsProvider.addIncludeKeyName(includedKeyValueKeyName); + } + } + public void setIncludeKeyValueKeyNames(List includeKeyValueKeyNames) { + if (isIncludeKeyValuePairs()) { + keyValuePairsProvider.setIncludeKeyNames(includeKeyValueKeyNames); + } + } + + public List getExcludeKeyValueKeyNames() { + return isIncludeKeyValuePairs() + ? keyValuePairsProvider.getExcludeKeyNames() + : Collections.emptyList(); + } + public void addExcludeKeyValueKeyName(String excludedKeyValueKeyName) { + if (isIncludeKeyValuePairs()) { + keyValuePairsProvider.addExcludeKeyName(excludedKeyValueKeyName); + } + } + public void setExcludeKeyValueKeyNames(List excludeKeyValueKeyNames) { + if (isIncludeKeyValuePairs()) { + keyValuePairsProvider.setExcludeKeyNames(excludeKeyValueKeyNames); + } + } + public void addKeyValueKeyFieldName(String keyValueKeyFieldName) { + if (isIncludeKeyValuePairs()) { + keyValuePairsProvider.addKeyFieldName(keyValueKeyFieldName); + } + } + public boolean isIncludeContext() { return contextProvider != null; } @@ -388,6 +450,9 @@ public void addProvider(JsonProvider provider) { } else if (provider instanceof MdcJsonProvider) { getProviders().removeProvider(this.mdcProvider); this.mdcProvider = (MdcJsonProvider) provider; + } else if (provider instanceof KeyValuePairsJsonProvider) { + getProviders().removeProvider(this.keyValuePairsProvider); + this.keyValuePairsProvider = (KeyValuePairsJsonProvider) provider; } getProviders().addProvider(provider); } diff --git a/src/main/java/net/logstash/logback/composite/loggingevent/KeyValuePairsJsonProvider.java b/src/main/java/net/logstash/logback/composite/loggingevent/KeyValuePairsJsonProvider.java new file mode 100644 index 00000000..f4e90e29 --- /dev/null +++ b/src/main/java/net/logstash/logback/composite/loggingevent/KeyValuePairsJsonProvider.java @@ -0,0 +1,162 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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. + */ +package net.logstash.logback.composite.loggingevent; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.logstash.logback.composite.AbstractFieldJsonProvider; +import net.logstash.logback.composite.FieldNamesAware; +import net.logstash.logback.fieldnames.LogstashFieldNames; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.fasterxml.jackson.core.JsonGenerator; +import org.slf4j.event.KeyValuePair; + +/** + * Includes key value pairs added from slf4j's fluent api in the output according to + * {@link #includeKeyNames} and {@link #excludeKeyNames}. + * + *

There are three valid combinations of {@link #includeKeyNames} + * and {@link #excludeKeyNames}:

+ * + *
    + *
  1. When {@link #includeKeyNames} and {@link #excludeKeyNames} + * are both empty, then all entries will be included.
  2. + *
  3. When {@link #includeKeyNames} is not empty and + * {@link #excludeKeyNames} is empty, then only those entries + * with key names in {@link #includeKeyNames} will be included.
  4. + *
  5. When {@link #includeKeyNames} is empty and + * {@link #excludeKeyNames} is not empty, then all entries except those + * with key names in {@link #excludeKeyNames} will be included.
  6. + *
+ * + *

It is a configuration error for both {@link #includeKeyNames} + * and {@link #excludeKeyNames} to be not empty.

+ * + *

By default, for each key value pair, the key is output as the field name. + * This can be changed by specifying an explicit field name to use for a ke + * via {@link #addKeyFieldName(String)}

+ * + *

If the fieldName is set, then the pairs will be written + * to that field as a subobject. + * Otherwise, the pairs are written inline.

+ */ +public class KeyValuePairsJsonProvider extends AbstractFieldJsonProvider implements FieldNamesAware { + + /** + * See {@link KeyValuePairsJsonProvider}. + */ + private List includeKeyNames = new ArrayList<>(); + + /** + * See {@link KeyValuePairsJsonProvider}. + */ + private List excludeKeyNames = new ArrayList<>(); + + private final Map keyFieldNames = new HashMap<>(); + + @Override + public void start() { + if (!this.includeKeyNames.isEmpty() && !this.excludeKeyNames.isEmpty()) { + addError("Both includeKeyNames and excludeKeyNames are not empty. Only one is allowed to be not empty."); + } + super.start(); + } + + @Override + public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException { + List keyValuePairs = event.getKeyValuePairs(); + if (keyValuePairs == null || keyValuePairs.isEmpty()) { + return; + } + + String fieldName = getFieldName(); + if (fieldName != null) { + generator.writeObjectFieldStart(getFieldName()); + } + + for (KeyValuePair keyValuePair : keyValuePairs) { + if (keyValuePair.key != null && keyValuePair.value != null + && (includeKeyNames.isEmpty() || includeKeyNames.contains(keyValuePair.key)) + && (excludeKeyNames.isEmpty() || !excludeKeyNames.contains(keyValuePair.key))) { + + String key = keyFieldNames.get(keyValuePair.key); + if (key == null) { + key = keyValuePair.key; + } + generator.writeFieldName(key); + generator.writeObject(keyValuePair.value); + } + } + + if (fieldName != null) { + generator.writeEndObject(); + } + } + + @Override + public void setFieldNames(LogstashFieldNames fieldNames) { + setFieldName(fieldNames.getKeyValuePair()); + } + + public List getIncludeKeyNames() { + return Collections.unmodifiableList(includeKeyNames); + } + + public void addIncludeKeyName(String includedKeyName) { + this.includeKeyNames.add(includedKeyName); + } + + public void setIncludeKeyNames(List includeKeyNames) { + this.includeKeyNames = new ArrayList<>(includeKeyNames); + } + + public List getExcludeKeyNames() { + return Collections.unmodifiableList(excludeKeyNames); + } + + public void addExcludeKeyName(String excludedKeyName) { + this.excludeKeyNames.add(excludedKeyName); + } + + public void setExcludeKeyNames(List excludeKeyNames) { + this.excludeKeyNames = new ArrayList<>(excludeKeyNames); + } + + public Map getKeyFieldNames() { + return keyFieldNames; + } + + /** + * Adds the given keyFieldName entry in the form keyName=fieldName + * to use an alternative field name for an KeyValuePair key. + * + * @param keyFieldName a string in the form kvpKeyName=fieldName that identifies what field name to use for a specific KeyValuePair key. + */ + public void addKeyFieldName(String keyFieldName) { + String[] split = keyFieldName.split("="); + if (split.length != 2) { + throw new IllegalArgumentException("keyFieldName (" + keyFieldName + ") must be in the form keyName=fieldName"); + } + keyFieldNames.put(split[0], split[1]); + } + +} diff --git a/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventJsonProviders.java b/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventJsonProviders.java index ef2e1bc6..01aa3667 100644 --- a/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventJsonProviders.java +++ b/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventJsonProviders.java @@ -73,6 +73,9 @@ public void addContextName(ContextNameJsonProvider provider) { public void addMdc(MdcJsonProvider provider) { addProvider(provider); } + public void addKeyValuePairs(KeyValuePairsJsonProvider provider) { + addProvider(provider); + } public void addTags(TagsJsonProvider provider) { addProvider(provider); } diff --git a/src/main/java/net/logstash/logback/encoder/LogstashEncoder.java b/src/main/java/net/logstash/logback/encoder/LogstashEncoder.java index 95bea9a3..90b1c4b7 100644 --- a/src/main/java/net/logstash/logback/encoder/LogstashEncoder.java +++ b/src/main/java/net/logstash/logback/encoder/LogstashEncoder.java @@ -110,6 +110,43 @@ public void addMdcKeyFieldName(String mdcKeyFieldName) { getFormatter().addMdcKeyFieldName(mdcKeyFieldName); } + + public boolean isIncludeKeyValuePairs() { + return getFormatter().isIncludeKeyValuePairs(); + } + + public void setIncludeKeyValuePairs(boolean includeKeyValuePairs) { + getFormatter().setIncludeKeyValuePairs(includeKeyValuePairs); + } + + public void addIncludeKeyValueKeyName(String includedKeyValueKeyName) { + getFormatter().addIncludeKeyValueKeyName(includedKeyValueKeyName); + } + + public List getIncludeKeyValueKeyNames() { + return getFormatter().getIncludeKeyValueKeyNames(); + } + + public void setIncludeKeyValueKeyNames(List includeKeyValueKeyNames) { + getFormatter().setIncludeKeyValueKeyNames(includeKeyValueKeyNames); + } + + public void addExcludeKeyValueKeyName(String excludedKeyValueKeyName) { + getFormatter().addExcludeKeyValueKeyName(excludedKeyValueKeyName); + } + + public List getExcludeKeyValueKeyNames() { + return getFormatter().getExcludeKeyValueKeyNames(); + } + + public void setExcludeKeyValueKeyNames(List excludedKeyValueKeyNames) { + getFormatter().setExcludeKeyValueKeyNames(excludedKeyValueKeyNames); + } + + public void addKeyValueKeyFieldName(String keyValueKeyFieldName) { + getFormatter().addKeyValueKeyFieldName(keyValueKeyFieldName); + } + public boolean isIncludeTags() { return getFormatter().isIncludeTags(); } diff --git a/src/main/java/net/logstash/logback/fieldnames/LogstashFieldNames.java b/src/main/java/net/logstash/logback/fieldnames/LogstashFieldNames.java index 358e1c1b..00352bca 100644 --- a/src/main/java/net/logstash/logback/fieldnames/LogstashFieldNames.java +++ b/src/main/java/net/logstash/logback/fieldnames/LogstashFieldNames.java @@ -42,6 +42,7 @@ public class LogstashFieldNames extends LogstashCommonFieldNames { private String rootStackTraceElementMethod = RootStackTraceElementJsonProvider.FIELD_METHOD_NAME; private String tags = TagsJsonProvider.FIELD_TAGS; private String mdc; + private String keyValuePair; private String arguments; public String getLogger() { @@ -150,6 +151,15 @@ public void setMdc(String mdc) { this.mdc = mdc; } + + public String getKeyValuePair() { + return keyValuePair; + } + + public void setKeyValuePair(String keyValuePair) { + this.keyValuePair = keyValuePair; + } + /** * The name of the arguments object field. *

diff --git a/src/main/java/net/logstash/logback/layout/LogstashLayout.java b/src/main/java/net/logstash/logback/layout/LogstashLayout.java index f7efd5b6..d264ef1a 100644 --- a/src/main/java/net/logstash/logback/layout/LogstashLayout.java +++ b/src/main/java/net/logstash/logback/layout/LogstashLayout.java @@ -101,6 +101,42 @@ public void addMdcKeyFieldName(String mdcKeyFieldName) { getFormatter().addMdcKeyFieldName(mdcKeyFieldName); } + public boolean isIncludeKeyValuePairs() { + return getFormatter().isIncludeKeyValuePairs(); + } + + public void setIncludeKeyValuePairs(boolean includeKeyValuePairs) { + getFormatter().setIncludeKeyValuePairs(includeKeyValuePairs); + } + + public void addIncludeKeyValueKeyName(String includedKeyValueKeyName) { + getFormatter().addIncludeKeyValueKeyName(includedKeyValueKeyName); + } + + public List getIncludeKeyValueKeyNames() { + return getFormatter().getIncludeKeyValueKeyNames(); + } + + public void setIncludeKeyValueKeyNames(List includeKeyValueKeyNames) { + getFormatter().setIncludeKeyValueKeyNames(includeKeyValueKeyNames); + } + + public void addExcludeKeyValueKeyName(String excludedKeyValueKeyName) { + getFormatter().addExcludeKeyValueKeyName(excludedKeyValueKeyName); + } + + public List getExcludeKeyValueKeyNames() { + return getFormatter().getExcludeKeyValueKeyNames(); + } + + public void setExcludeKeyValueKeyNames(List excludedKeyValueKeyNames) { + getFormatter().setExcludeKeyValueKeyNames(excludedKeyValueKeyNames); + } + + public void addKeyValueKeyFieldName(String keyValueKeyFieldName) { + getFormatter().addKeyValueKeyFieldName(keyValueKeyFieldName); + } + public boolean isIncludeTags() { return getFormatter().isIncludeTags(); } diff --git a/src/test/java/net/logstash/logback/ConfigurationTest.java b/src/test/java/net/logstash/logback/ConfigurationTest.java index 3de48a36..4e381a58 100644 --- a/src/test/java/net/logstash/logback/ConfigurationTest.java +++ b/src/test/java/net/logstash/logback/ConfigurationTest.java @@ -37,6 +37,7 @@ import net.logstash.logback.composite.loggingevent.ArgumentsJsonProvider; import net.logstash.logback.composite.loggingevent.CallerDataJsonProvider; import net.logstash.logback.composite.loggingevent.ContextNameJsonProvider; +import net.logstash.logback.composite.loggingevent.KeyValuePairsJsonProvider; import net.logstash.logback.composite.loggingevent.LogLevelJsonProvider; import net.logstash.logback.composite.loggingevent.LogLevelValueJsonProvider; import net.logstash.logback.composite.loggingevent.LoggerNameJsonProvider; @@ -87,7 +88,7 @@ public void testLogstashEncoderAppender() throws IOException { LoggingEventCompositeJsonEncoder encoder = getEncoder("logstashEncoderAppender"); List> providers = encoder.getProviders().getProviders(); - assertThat(providers).hasSize(19); + assertThat(providers).hasSize(20); verifyCommonProviders(providers); @@ -99,7 +100,7 @@ public void testLoggingEventCompositeJsonEncoderAppender() throws IOException { LoggingEventCompositeJsonEncoder encoder = getEncoder("loggingEventCompositeJsonEncoderAppender"); List> providers = encoder.getProviders().getProviders(); - assertThat(providers).hasSize(23); + assertThat(providers).hasSize(24); verifyCommonProviders(providers); @@ -178,7 +179,11 @@ private void verifyCommonProviders(List> providers) assertThat(mdcJsonProvider).isNotNull(); assertThat(mdcJsonProvider.getIncludeMdcKeyNames()).containsExactly("included"); assertThat(mdcJsonProvider.getMdcKeyFieldNames()).containsOnly(entry("key", "renamedKey")); - + + KeyValuePairsJsonProvider keyValuePairsJsonProvider = getInstance(providers, KeyValuePairsJsonProvider.class); + assertThat(keyValuePairsJsonProvider).isNotNull(); + assertThat(keyValuePairsJsonProvider.getIncludeKeyNames()).containsExactly("included"); + assertThat(keyValuePairsJsonProvider.getKeyFieldNames()).containsOnly(entry("key", "renamedKey")); GlobalCustomFieldsJsonProvider globalCustomFieldsJsonProvider = getInstance(providers, GlobalCustomFieldsJsonProvider.class); assertThat(globalCustomFieldsJsonProvider).isNotNull(); diff --git a/src/test/java/net/logstash/logback/composite/loggingevent/KeyValuePairsJsonProviderTest.java b/src/test/java/net/logstash/logback/composite/loggingevent/KeyValuePairsJsonProviderTest.java new file mode 100644 index 00000000..e7762cdb --- /dev/null +++ b/src/test/java/net/logstash/logback/composite/loggingevent/KeyValuePairsJsonProviderTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * 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. + */ +package net.logstash.logback.composite.loggingevent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.logstash.logback.fieldnames.LogstashFieldNames; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.event.KeyValuePair; + +@ExtendWith(MockitoExtension.class) +public class KeyValuePairsJsonProviderTest { + + private KeyValuePairsJsonProvider provider = new KeyValuePairsJsonProvider(); + + private ByteArrayOutputStream resultStream; + private JsonGenerator generator; + + @Mock + private ILoggingEvent event; + + private List keyValuePairs; + + @BeforeEach + public void setup() throws Exception { + keyValuePairs = new ArrayList<>(); + keyValuePairs.add(new KeyValuePair("name1", "value1")); + keyValuePairs.add(new KeyValuePair("name2", 2023)); + keyValuePairs.add(new KeyValuePair("name3", new TestValue())); + when(event.getKeyValuePairs()).thenReturn(keyValuePairs); + resultStream = new ByteArrayOutputStream(); + generator = new JsonFactory().createGenerator(resultStream); + generator.setCodec(new ObjectMapper()); + } + + @Test + public void testUnwrapped() throws IOException { + assertThat(generateJson()) + .isEqualTo("{\"name1\":\"value1\",\"name2\":2023,\"name3\":{\"a\":1}}"); + } + + @Test + public void testWrapped() throws IOException { + provider.setFieldName("kvp"); + assertThat(generateJson()) + .isEqualTo("{\"kvp\":{\"name1\":\"value1\",\"name2\":2023,\"name3\":{\"a\":1}}}"); + } + + @Test + public void testWrappedUsingFieldNames() throws IOException { + LogstashFieldNames fieldNames = new LogstashFieldNames(); + fieldNames.setKeyValuePair("kvp"); + provider.setFieldNames(fieldNames); + assertThat(generateJson()) + .isEqualTo("{\"kvp\":{\"name1\":\"value1\",\"name2\":2023,\"name3\":{\"a\":1}}}"); + } + + @Test + public void testInclude() throws IOException { + provider.setIncludeKeyNames(Collections.singletonList("name1")); + + assertThat(generateJson()) + .isEqualTo("{\"name1\":\"value1\"}"); + } + + @Test + public void testExclude() throws IOException { + provider.setExcludeKeyNames(Collections.singletonList("name1")); + + assertThat(generateJson()) + .isEqualTo("{\"name2\":2023,\"name3\":{\"a\":1}}"); + } + + @Test + public void testAlternativeFieldName() throws IOException { + provider.addKeyFieldName("name1=alternativeName1"); + + assertThat(generateJson()) + .isEqualTo("{\"alternativeName1\":\"value1\",\"name2\":2023,\"name3\":{\"a\":1}}"); + } + + private String generateJson() throws IOException { + generator.writeStartObject(); + provider.writeTo(generator, event); + generator.writeEndObject(); + + generator.flush(); + return resultStream.toString(); + } + + private class TestValue { + private final int a = 1; + + public int getA() { + return a; + } + } +} diff --git a/src/test/java/net/logstash/logback/encoder/LogstashEncoderTest.java b/src/test/java/net/logstash/logback/encoder/LogstashEncoderTest.java index 083cbb72..935a50ad 100644 --- a/src/test/java/net/logstash/logback/encoder/LogstashEncoderTest.java +++ b/src/test/java/net/logstash/logback/encoder/LogstashEncoderTest.java @@ -30,6 +30,7 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -62,6 +63,7 @@ import org.slf4j.MDC; import org.slf4j.Marker; import org.slf4j.MarkerFactory; +import org.slf4j.event.KeyValuePair; public class LogstashEncoderTest { @@ -293,6 +295,111 @@ public void nullMDCDoesNotCauseEverythingToBlowUp() { assertThatCode(() -> encoder.encode(event)).doesNotThrowAnyException(); } + @Test + public void kvpAllIncluded() throws Exception { + List kvp = new ArrayList<>(); + kvp.add(new KeyValuePair("thing_one", "One")); + kvp.add(new KeyValuePair("thing_two", "Three")); + + LoggingEvent event = mockBasicILoggingEvent(Level.ERROR); + event.setKeyValuePairs(kvp); + + encoder.start(); + byte[] encoded = encoder.encode(event); + + JsonNode node = MAPPER.readTree(encoded); + + assertThat(node.get("thing_one").textValue()).isEqualTo("One"); + assertThat(node.get("thing_two").textValue()).isEqualTo("Three"); + } + + @Test + public void kvpSomeIncluded() throws Exception { + List kvp = new ArrayList<>(); + kvp.add(new KeyValuePair("thing_one", "One")); + kvp.add(new KeyValuePair("thing_two", "Three")); + + LoggingEvent event = mockBasicILoggingEvent(Level.ERROR); + event.setKeyValuePairs(kvp); + + encoder.addIncludeKeyValueKeyName("thing_one"); + + encoder.start(); + byte[] encoded = encoder.encode(event); + + JsonNode node = MAPPER.readTree(encoded); + + assertThat(node.get("thing_one").textValue()).isEqualTo("One"); + assertThat(node.get("thing_two")).isNull(); + } + + @Test + public void kvpSomeExcluded() throws Exception { + List kvp = new ArrayList<>(); + kvp.add(new KeyValuePair("thing_one", "One")); + kvp.add(new KeyValuePair("thing_two", "Three")); + + LoggingEvent event = mockBasicILoggingEvent(Level.ERROR); + event.setKeyValuePairs(kvp); + + encoder.addExcludeKeyValueKeyName("thing_two"); + + encoder.start(); + byte[] encoded = encoder.encode(event); + + JsonNode node = MAPPER.readTree(encoded); + + assertThat(node.get("thing_one").textValue()).isEqualTo("One"); + assertThat(node.get("thing_two")).isNull(); + } + + @Test + public void kvpNoneIncluded() throws Exception { + List kvp = new ArrayList<>(); + kvp.add(new KeyValuePair("thing_one", "One")); + kvp.add(new KeyValuePair("thing_two", "Three")); + + LoggingEvent event = mockBasicILoggingEvent(Level.ERROR); + event.setKeyValuePairs(kvp); + + encoder.setIncludeKeyValuePairs(false); + encoder.start(); + byte[] encoded = encoder.encode(event); + + JsonNode node = MAPPER.readTree(encoded); + + assertThat(node.get("thing_one")).isNull(); + assertThat(node.get("thing_two")).isNull(); + } + + @Test + public void propertiesInKVPAreIncludedInSubObject() throws Exception { + List kvp = new ArrayList<>(); + kvp.add(new KeyValuePair("thing_one", "One")); + kvp.add(new KeyValuePair("thing_two", "Three")); + + LoggingEvent event = mockBasicILoggingEvent(Level.ERROR); + event.setKeyValuePairs(kvp); + + encoder.getFieldNames().setKeyValuePair("kvp"); + encoder.start(); + byte[] encoded = encoder.encode(event); + + JsonNode node = MAPPER.readTree(encoded); + + assertThat(node.get("kvp").get("thing_one").textValue()).isEqualTo("One"); + assertThat(node.get("kvp").get("thing_two").textValue()).isEqualTo("Three"); + } + + @Test + public void nullKVPDoesNotCauseEverythingToBlowUp() { + LoggingEvent event = mockBasicILoggingEvent(Level.ERROR); + event.setKeyValuePairs(null); + + encoder.start(); + assertThatCode(() -> encoder.encode(event)).doesNotThrowAnyException(); + } + @Test public void callerDataIsIncluded() throws Exception { LoggingEvent event = mockBasicILoggingEvent(Level.ERROR); @@ -602,6 +709,38 @@ public void testEncoderConfiguration() throws Exception { "{ \"version\" : \"Version 0.1.0-SNAPSHOT\", \"lastcommit\" : \"75473700d5befa953c45f630c6d9105413c16fe1\"}")); } + @Test + public void testEncoderConfigurationOnFluentApi() throws Exception { + Logger LOG = LoggerFactory.getLogger(LogstashEncoderTest.class); + + File tempFile = new File(System.getProperty("java.io.tmpdir"), "test.log"); + // Empty the log file + PrintWriter writer = new PrintWriter(tempFile); + writer.print(""); + writer.close(); + LOG.atInfo() + .addMarker(append("appendedName", "appendedValue").and(append("n1", 2))) + .addKeyValue("myKvpKey", "myKvpValue") + .log("Testing info logging."); + + List lines = Files.linesOf(tempFile, StandardCharsets.UTF_8); + JsonNode node = MAPPER.readTree(lines.get(0).getBytes(StandardCharsets.UTF_8)); + + /* + * The configuration suppresses the version field, make sure it doesn't appear. + */ + assertThat(node.get("@version")).isNull(); + assertThat(node.get(LogstashCommonFieldNames.IGNORE_FIELD_INDICATOR)).isNull(); + + assertThat(node.get("appname").textValue()).isEqualTo("damnGodWebservice"); + assertThat(node.get("appendedName").textValue()).isEqualTo("appendedValue"); + assertThat(node.get("myKvpKey").textValue()).isEqualTo("myKvpValue"); + assertThat(node.get("logger").textValue()).isEqualTo(LogstashEncoderTest.class.getName()); + assertThat(node.get("roles")).isEqualTo(parse("[\"customerorder\", \"auth\"]")); + assertThat(node.get("buildinfo")).isEqualTo(parse( + "{ \"version\" : \"Version 0.1.0-SNAPSHOT\", \"lastcommit\" : \"75473700d5befa953c45f630c6d9105413c16fe1\"}")); + } + @Test public void testCustomFields() { String customFields = "{\"foo\":\"bar\"}"; diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 98a03180..8c756cc6 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -50,6 +50,8 @@ included key=renamedKey + included + key=renamedKey {"customName":"customValue"} true @@ -139,6 +141,10 @@ included key=renamedKey + + included + key=renamedKey + {"customName":"customValue"}