diff --git a/README.md b/README.md
index 36871477..35d0fb42 100644
--- a/README.md
+++ b/README.md
@@ -1964,7 +1964,7 @@ The provider name is the xml element name to use when configuring.
- fieldName - Output field name (@timestamp)
- pattern - Output format ([ISO_OFFSET_DATE_TIME]) See above for possible values.
- - timeZone - Timezone (local timezone)
+ - timeZone - Timezone (system timezone)
@@ -2130,7 +2130,7 @@ The provider name is the xml element name to use when configuring. Each provider
- fieldName - Output field name (stack_hash)
- exclude - Regular expression pattern matching stack trace elements to exclude when computing the error hash
- - exclusions - Coma separated list of regular expression patterns matching stack trace elements to exclude when computing the error hash
+ - exclusions - Comma separated list of regular expression patterns matching stack trace elements to exclude when computing the error hash
@@ -2207,7 +2207,7 @@ The provider name is the xml element name to use when configuring. Each provider
- fieldName - Output field name (message)
- pattern - Output format of the timestamp ([ISO_OFFSET_DATE_TIME]). See above for possible values.
- - timeZone - Timezone (local timezone)
+ - timeZone - Timezone (system timezone)
@@ -2383,10 +2383,10 @@ even for something which you may feel should be a number - like for `%b` (bytes
You can either deal with the type conversion on the logstash side or you may use special operations provided by this encoder.
The operations are:
-* `#asLong{...}` - evaluates the pattern in curly braces and then converts resulting string to a Long (or a null if conversion fails).
-* `#asDouble{...}` - evaluates the pattern in curly braces and then converts resulting string to a Double (or a null if conversion fails).
-* `#asBoolean{...}`- evaluates the pattern in curly braces and then converts resulting string to a Boolean. Conversion is case insensitive. `true`, `yes`, `y` and `1` (case insensitive) are converted to a boolean `true`, a null or empty string is converted to `null`, anything else returns `false`.
-* `#asJson{...}` - evaluates the pattern in curly braces and then converts resulting string to json (or a null if conversion fails).
+* `#asLong{...}` - evaluates the pattern in curly braces and then converts resulting string to a Long (or a `null` if conversion fails).
+* `#asDouble{...}` - evaluates the pattern in curly braces and then converts resulting string to a Double (or a `null` if conversion fails).
+* `#asBoolean{...}`- evaluates the pattern in curly braces and then converts resulting string to a Boolean. Conversion is case insensitive. `true`, `yes`, `y` and `1` (case insensitive) are converted to a boolean `true`, a `null` or empty string is converted to `null`, anything else returns `false`.
+* `#asJson{...}` - evaluates the pattern in curly braces and then converts resulting string to json (or a `null` if conversion fails).
* `#tryJson{...}` - evaluates the pattern in curly braces and then converts resulting string to json (or just the string if conversion fails).
So this example...
@@ -2402,7 +2402,7 @@ So this example...
```
-... And this logging code...
+... and this logging code...
```java
MDC.put("hasMessage", "true");
@@ -2470,7 +2470,7 @@ If the MDC did not contain a `traceId` entry, then a JSON log event from the abo
#### LoggingEvent patterns
-For LoggingEvents, patterns from logback-classic's
+For LoggingEvents, conversion specifiers from logback-classic's
[`PatternLayout`](http://logback.qos.ch/manual/layouts.html#conversionWord) are supported.
For example:
@@ -2496,10 +2496,14 @@ For example:
```
+Note that the [`%property{key}`](https://logback.qos.ch/manual/layouts.html#property) conversion specifier behaves slightly differently when used in the context of the Pattern Json provider. If the property cannot be found in the logger context or the System properties, it returns **an empty string** instead of `null` as it would normally do. For example, assuming the "foo" property is not defined, `%property{foo}` would return `""` (an empty string) instead of `"null"` (a string whose content is made of 4 letters).
+
+The _property_ conversion specifier also allows you to specify a default value to use when the property is not defined. The default value is optional and can be specified using the `:-` operator as in Bash shell. For example, assuming the "foo" property is not defined, `%property{foo:-bar}` will return `bar`.
+
#### AccessEvent patterns
-For AccessEvents, patterns from logback-access's
+For AccessEvents, conversion specifiers from logback-access's
[`PatternLayout`](http://logback.qos.ch/xref/ch/qos/logback/access/PatternLayout.html) are supported.
For example:
@@ -2529,9 +2533,9 @@ For example:
There is also a special operation that can be used with this AccessEvents:
-* `#nullNA{...}` - if the pattern in curly braces evaluates to a dash ("-"), it will be replaced with a null value.
+* `#nullNA{...}` - if the pattern in curly braces evaluates to a dash (`-`), it will be replaced with a `null` value.
-You may want to use it because many of the `PatternLayout` conversion specifiers from logback-access will evaluate to "-"
+You may want to use it because many of the `PatternLayout` conversion words from logback-access will evaluate to `-`
for non-existent value (for example for a cookie, header or a request attribute).
So the following pattern...
diff --git a/src/main/java/net/logstash/logback/pattern/EnhancedPropertyConverter.java b/src/main/java/net/logstash/logback/pattern/EnhancedPropertyConverter.java
new file mode 100644
index 00000000..a45d9ca5
--- /dev/null
+++ b/src/main/java/net/logstash/logback/pattern/EnhancedPropertyConverter.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2013-2021 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.pattern;
+
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import ch.qos.logback.classic.pattern.ClassicConverter;
+import ch.qos.logback.classic.pattern.PropertyConverter;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.LoggerContextVO;
+
+/**
+ * Variation of the Logback {@link PropertyConverter} with the option to specify a default
+ * value to use when the property does not exist instead of returning {@code null} as does
+ * the original Logback implementation.
+ *
+ * The default value is optional and can be specified using the :- operator as
+ * in Bash shell. For example, assuming the property "foo" is not defined, %property{foo:-bar}
+ * will return bar.
+ * If no optional value is declared, the converter returns an empty string instead of {@code null}
+ * if the property is not defined.
+ *
+ *
The property resolution mechanism is the same as the Logback implementation. The property is
+ * first looked up in the context associated with the logging event. If not found, the property is
+ * searched in the System environment.
+ *
+ *
+ * @author brenuart
+ */
+public class EnhancedPropertyConverter extends ClassicConverter {
+ /**
+ * Regex pattern used to extract the optional default value from the key name (split
+ * at the first :-).
+ */
+ private static final Pattern PATTERN = Pattern.compile("(.+?):-(.*)");
+
+ /**
+ * The property name.
+ */
+ private String propertyName;
+
+ /**
+ * The default value to use when the property is not defined.
+ */
+ private String defaultValue = "";
+
+ public void start() {
+ String optStr = getFirstOption();
+ if (optStr != null) {
+ propertyName = optStr;
+ super.start();
+ }
+ if (propertyName == null) {
+ throw new IllegalStateException("Property name is not specified");
+ }
+
+ Matcher matcher = PATTERN.matcher(propertyName);
+ if (matcher.matches()) {
+ propertyName = matcher.group(1);
+ defaultValue = matcher.group(2);
+ }
+ }
+
+ @Override
+ public String convert(ILoggingEvent event) {
+ LoggerContextVO lcvo = event.getLoggerContextVO();
+ Map map = lcvo.getPropertyMap();
+ String val = map.get(propertyName);
+
+ if (val == null) {
+ val = System.getProperty(propertyName);
+ }
+
+ if (val == null) {
+ val = defaultValue;
+ }
+
+ return val;
+ }
+}
diff --git a/src/main/java/net/logstash/logback/pattern/LoggingEventJsonPatternParser.java b/src/main/java/net/logstash/logback/pattern/LoggingEventJsonPatternParser.java
index 5f254d38..37ddd4d4 100644
--- a/src/main/java/net/logstash/logback/pattern/LoggingEventJsonPatternParser.java
+++ b/src/main/java/net/logstash/logback/pattern/LoggingEventJsonPatternParser.java
@@ -30,9 +30,16 @@ public LoggingEventJsonPatternParser(final Context context, final JsonFactory js
super(context, jsonFactory);
}
+ /**
+ * Create a new {@link PatternLayout} and replace the default {@code %property} converter
+ * with a {@link EnhancedPropertyConverter} to add support for default value in case the
+ * property is not defined.
+ */
@Override
protected PatternLayoutBase createLayout() {
- return new PatternLayout();
+ PatternLayoutBase layout = new PatternLayout();
+ layout.getInstanceConverterMap().put("property", EnhancedPropertyConverter.class.getName());
+ return layout;
}
}
diff --git a/src/main/java/net/logstash/logback/pattern/PatternLayoutAdapter.java b/src/main/java/net/logstash/logback/pattern/PatternLayoutAdapter.java
index 568f18d4..9c6b25e0 100644
--- a/src/main/java/net/logstash/logback/pattern/PatternLayoutAdapter.java
+++ b/src/main/java/net/logstash/logback/pattern/PatternLayoutAdapter.java
@@ -144,7 +144,7 @@ public void writeTo(StringBuilder strBuilder, E event) {
}
/**
- * Indicate whether the {@link PatternLayoutBase} always generates the same constant value independent of
+ * Indicate whether the {@link PatternLayoutBase} always generates the same constant value regardless of
* the event it is given.
*
* @return true if the pattern is constant
diff --git a/src/test/java/net/logstash/logback/pattern/LoggingEventJsonPatternParserTest.java b/src/test/java/net/logstash/logback/pattern/LoggingEventJsonPatternParserTest.java
index 79e90fc5..37c3d7d1 100644
--- a/src/test/java/net/logstash/logback/pattern/LoggingEventJsonPatternParserTest.java
+++ b/src/test/java/net/logstash/logback/pattern/LoggingEventJsonPatternParserTest.java
@@ -15,11 +15,14 @@
*/
package net.logstash.logback.pattern;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;
import java.util.HashMap;
import java.util.Map;
+import net.logstash.logback.pattern.AbstractJsonPatternParser.JsonPatternException;
+
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Context;
@@ -46,6 +49,7 @@ protected ILoggingEvent createEvent() {
mdc.put("key2", "value2");
given(event.getMDCPropertyMap()).willReturn(mdc);
given(event.getLevel()).willReturn(Level.DEBUG);
+ given(event.getLoggerContextVO()).willAnswer(invocation -> context.getLoggerContextRemoteView());
return event;
}
@@ -55,7 +59,7 @@ protected AbstractJsonPatternParser createParser(Context context,
}
@Test
- public void shouldRunPatternLayoutConversions() throws Exception {
+ public void shouldRunPatternLayoutConversions() throws JsonPatternException {
String pattern = toJson(
"{ "
@@ -71,7 +75,7 @@ public void shouldRunPatternLayoutConversions() throws Exception {
}
@Test
- public void shouldAllowIndividualMdcItemsToBeIncludedUsingConverter() throws Exception {
+ public void shouldAllowIndividualMdcItemsToBeIncludedUsingConverter() throws JsonPatternException {
String pattern = toJson(
"{ "
@@ -87,7 +91,7 @@ public void shouldAllowIndividualMdcItemsToBeIncludedUsingConverter() throws Exc
}
@Test
- public void shouldOmitNullMdcValue() throws Exception {
+ public void shouldOmitNullMdcValue() throws JsonPatternException {
parser.setOmitEmptyFields(true);
String pattern = toJson(
@@ -103,4 +107,53 @@ public void shouldOmitNullMdcValue() throws Exception {
verifyFields(pattern, expected);
}
+
+
+ @Test
+ public void propertyDefined() throws JsonPatternException {
+ context.putProperty("PROP", "value");
+
+ String pattern = toJson(
+ "{ "
+ + " 'prop1': '%property{PROP}', "
+ + " 'prop2': '%property{PROP:-}', "
+ + " 'prop3': '%property{PROP:-foo}' "
+ + "} ");
+
+ String expected = toJson(
+ "{ "
+ + " 'prop1': 'value', "
+ + " 'prop2': 'value', "
+ + " 'prop3': 'value' "
+ + "} ");
+
+ verifyFields(pattern, expected);
+ }
+
+
+ @Test
+ public void propertyUndefined() throws JsonPatternException {
+
+ String pattern = toJson(
+ "{ "
+ + " 'prop1': '%property{PROP}', "
+ + " 'prop2': '%property{PROP:-}', "
+ + " 'prop3': '%property{PROP:-foo}' "
+ + "} ");
+
+ String expected = toJson(
+ "{ "
+ + " 'prop1': '', "
+ + " 'prop2': '', "
+ + " 'prop3': 'foo' "
+ + "} ");
+
+ verifyFields(pattern, expected);
+ }
+
+
+ @Test
+ public void propertyInvalid() throws JsonPatternException {
+ assertThatThrownBy(() -> parser.parse(toJson("{ 'prop': '%property{}' }"))).isInstanceOf(JsonPatternException.class);
+ }
}