Skip to content

Commit 568466b

Browse files
authored
Add MDC entry writers to be able to convert values (#957)
1 parent d23b70d commit 568466b

File tree

16 files changed

+668
-6
lines changed

16 files changed

+668
-6
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,51 @@ specify `<mdcKeyFieldName>mdcKeyName=fieldName</mdcKeyFieldName>`:
10301030
</encoder>
10311031
```
10321032

1033+
You can also manipulate the MDC entry values written to the JSON output.
1034+
By default, no manipulations are done and all MDC entry values are written as text.
1035+
1036+
Currently, MDC entry writers for the following value types are supported:
1037+
1038+
```xml
1039+
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
1040+
<!--
1041+
Writes long values (instead of String values) for any MDC values
1042+
that can be parsed as a long (radix 10).
1043+
e.g. Writes 1234 instead of "1234"
1044+
-->
1045+
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.LongMdcEntryWriter"/>
1046+
1047+
<!--
1048+
Writes double values (instead of String values) for any MDC values
1049+
that can be parsed as a double, except NaN and positive/negative Infinity.
1050+
e.g. 1234.5678 instead of "1234.5678"
1051+
-->
1052+
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.DoubleMdcEntryWriter"/>
1053+
1054+
<!--
1055+
Writes boolean values (instead of String values) for any MDC values
1056+
that equal "true" or "false", ignoring case.
1057+
e.g. Writes true instead of "true"
1058+
-->
1059+
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.BooleanMdcEntryWriter"/>
1060+
</encoder>
1061+
```
1062+
1063+
To add your own MDC entry writer for other types or apply the manipulations only for specific fields
1064+
you can write your own implementation of [`MdcEntryWriter`](src/main/java/net/logstash/logback/composite/loggingevent/mdc/MdcEntryWriter.java).
1065+
1066+
You can also replace the default MDC JSON provider with your own class extending from
1067+
[`MdcJsonProvider`](src/main/java/net/logstash/logback/composite/loggingevent/MdcJsonProvider.java).
1068+
Configuring your class as a [Custom JSON Provider](#custom-json-provider) will then replace
1069+
the default `MdcJsonProvider`.
1070+
1071+
```xml
1072+
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
1073+
<provider class="mypackagenames.MyCustomMdcJsonProvider"/>
1074+
</encoder>
1075+
```
1076+
1077+
10331078
### Key Value Pair Fields
10341079

10351080
Slf4j 2's [fluent API](https://www.slf4j.org/manual.html#fluent) supports attaching key value pairs to the log event.
@@ -1071,6 +1116,7 @@ specify`<keyValuePairsKeyFieldName>keyName=fieldName</keyValuePairsKeyFieldName>
10711116
</encoder>
10721117
```
10731118

1119+
10741120
### Context fields
10751121

10761122
By default, each property of Logback's Context (`ch.qos.logback.core.Context`)

src/main/java/net/logstash/logback/LogstashFormatter.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import net.logstash.logback.composite.loggingevent.MessageJsonProvider;
4040
import net.logstash.logback.composite.loggingevent.StackTraceJsonProvider;
4141
import net.logstash.logback.composite.loggingevent.TagsJsonProvider;
42+
import net.logstash.logback.composite.loggingevent.mdc.MdcEntryWriter;
4243
import net.logstash.logback.fieldnames.LogstashFieldNames;
4344

4445
import ch.qos.logback.classic.pattern.ThrowableHandlingConverter;
@@ -322,6 +323,16 @@ public void addMdcKeyFieldName(String mdcKeyFieldName) {
322323
mdcProvider.addMdcKeyFieldName(mdcKeyFieldName);
323324
}
324325
}
326+
public List<MdcEntryWriter> getMdcEntryWriters() {
327+
return isIncludeMdc()
328+
? mdcProvider.getMdcEntryWriters()
329+
: Collections.emptyList();
330+
}
331+
public void addMdcEntryWriter(MdcEntryWriter mdcEntryWriter) {
332+
if (isIncludeMdc()) {
333+
mdcProvider.addMdcEntryWriter(mdcEntryWriter);
334+
}
335+
}
325336

326337
public List<String> getIncludeKeyValueKeyNames() {
327338
return isIncludeKeyValuePairs()

src/main/java/net/logstash/logback/composite/loggingevent/MdcJsonProvider.java

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import net.logstash.logback.composite.AbstractFieldJsonProvider;
2626
import net.logstash.logback.composite.FieldNamesAware;
27+
import net.logstash.logback.composite.loggingevent.mdc.MdcEntryWriter;
2728
import net.logstash.logback.fieldnames.LogstashFieldNames;
2829

2930
import ch.qos.logback.classic.spi.ILoggingEvent;
@@ -58,20 +59,28 @@
5859
* <p>If the fieldName is set, then the properties will be written
5960
* to that field as a subobject.
6061
* Otherwise, the properties are written inline.</p>
62+
*
63+
* <p>The output of the MDC entry values can be manipulated by the provided
64+
* {@link #mdcEntryWriters}. By default, all MDC entry values are written as texts.
6165
*/
6266
public class MdcJsonProvider extends AbstractFieldJsonProvider<ILoggingEvent> implements FieldNamesAware<LogstashFieldNames> {
6367

6468
/**
6569
* See {@link MdcJsonProvider}.
6670
*/
67-
private List<String> includeMdcKeyNames = new ArrayList<>();
71+
protected List<String> includeMdcKeyNames = new ArrayList<>();
6872

6973
/**
7074
* See {@link MdcJsonProvider}.
7175
*/
72-
private List<String> excludeMdcKeyNames = new ArrayList<>();
76+
protected List<String> excludeMdcKeyNames = new ArrayList<>();
77+
78+
protected final Map<String, String> mdcKeyFieldNames = new HashMap<>();
7379

74-
private final Map<String, String> mdcKeyFieldNames = new HashMap<>();
80+
/**
81+
* See {@link MdcJsonProvider}.
82+
*/
83+
protected final List<MdcEntryWriter> mdcEntryWriters = new ArrayList<>();
7584

7685
@Override
7786
public void start() {
@@ -101,8 +110,7 @@ public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOExcep
101110
generator.writeObjectFieldStart(getFieldName());
102111
hasWrittenStart = true;
103112
}
104-
generator.writeFieldName(fieldName);
105-
generator.writeObject(entry.getValue());
113+
writeMdcEntry(generator, fieldName, entry.getKey(), entry.getValue());
106114
}
107115
}
108116
if (hasWrittenStart) {
@@ -119,19 +127,23 @@ public void setFieldNames(LogstashFieldNames fieldNames) {
119127
public List<String> getIncludeMdcKeyNames() {
120128
return Collections.unmodifiableList(includeMdcKeyNames);
121129
}
130+
122131
public void addIncludeMdcKeyName(String includedMdcKeyName) {
123132
this.includeMdcKeyNames.add(includedMdcKeyName);
124133
}
134+
125135
public void setIncludeMdcKeyNames(List<String> includeMdcKeyNames) {
126-
this.includeMdcKeyNames = new ArrayList<String>(includeMdcKeyNames);
136+
this.includeMdcKeyNames = new ArrayList<>(includeMdcKeyNames);
127137
}
128138

129139
public List<String> getExcludeMdcKeyNames() {
130140
return Collections.unmodifiableList(excludeMdcKeyNames);
131141
}
142+
132143
public void addExcludeMdcKeyName(String excludedMdcKeyName) {
133144
this.excludeMdcKeyNames.add(excludedMdcKeyName);
134145
}
146+
135147
public void setExcludeMdcKeyNames(List<String> excludeMdcKeyNames) {
136148
this.excludeMdcKeyNames = new ArrayList<>(excludeMdcKeyNames);
137149
}
@@ -140,6 +152,13 @@ public Map<String, String> getMdcKeyFieldNames() {
140152
return mdcKeyFieldNames;
141153
}
142154

155+
public List<MdcEntryWriter> getMdcEntryWriters() {
156+
return Collections.unmodifiableList(mdcEntryWriters);
157+
}
158+
public void addMdcEntryWriter(MdcEntryWriter mdcEntryWriter) {
159+
this.mdcEntryWriters.add(mdcEntryWriter);
160+
}
161+
143162
/**
144163
* Adds the given mdcKeyFieldName entry in the form mdcKeyName=fieldName
145164
* to use an alternative field name for an MDC key.
@@ -154,4 +173,26 @@ public void addMdcKeyFieldName(String mdcKeyFieldName) {
154173
mdcKeyFieldNames.put(split[0], split[1]);
155174
}
156175

176+
/**
177+
* Writes the MDC entry with the given generator by iterating over the chain of {@link #mdcEntryWriters}
178+
* in the given order till the first {@link MdcEntryWriter} returns true.
179+
* <p>
180+
* If none of the {@link #mdcEntryWriters} returned true, the MDC field is written as String value by default.
181+
*
182+
* @param generator the generator to write the entry to.
183+
* @param fieldName the field name to use when writing the entry.
184+
* @param mdcKey the key of the MDC map entry.
185+
* @param mdcValue the value of the MDC map entry.
186+
*/
187+
private void writeMdcEntry(JsonGenerator generator, String fieldName, String mdcKey, String mdcValue) throws IOException {
188+
for (MdcEntryWriter mdcEntryWriter : this.mdcEntryWriters) {
189+
if (mdcEntryWriter.writeMdcEntry(generator, fieldName, mdcKey, mdcValue)) {
190+
return;
191+
}
192+
}
193+
194+
generator.writeFieldName(fieldName);
195+
generator.writeObject(mdcValue);
196+
}
197+
157198
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2013-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package net.logstash.logback.composite.loggingevent.mdc;
17+
18+
import java.io.IOException;
19+
20+
import com.fasterxml.jackson.core.JsonGenerator;
21+
22+
/**
23+
* Writes boolean values (instead of String values) for any MDC values that equal "true" or "false", ignoring case.
24+
*/
25+
public class BooleanMdcEntryWriter implements MdcEntryWriter {
26+
27+
@Override
28+
public boolean writeMdcEntry(JsonGenerator generator, String fieldName, String mdcKey, String mdcValue) throws IOException {
29+
if ("true".equalsIgnoreCase(mdcValue)) {
30+
generator.writeFieldName(fieldName);
31+
generator.writeBoolean(true);
32+
return true;
33+
}
34+
if ("false".equalsIgnoreCase(mdcValue)) {
35+
generator.writeFieldName(fieldName);
36+
generator.writeBoolean(false);
37+
return true;
38+
}
39+
40+
return false;
41+
}
42+
43+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2013-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package net.logstash.logback.composite.loggingevent.mdc;
17+
18+
import java.io.IOException;
19+
import java.util.regex.Pattern;
20+
21+
import com.fasterxml.jackson.core.JsonGenerator;
22+
23+
/**
24+
* Writes double values (instead of String values) for any MDC values that can be parsed as a double,
25+
* except NaN and positive/negative Infinity.
26+
*/
27+
public class DoubleMdcEntryWriter implements MdcEntryWriter {
28+
29+
private static final Pattern DOUBLE_PATTERN = doublePattern();
30+
31+
@Override
32+
public boolean writeMdcEntry(JsonGenerator generator, String fieldName, String mdcKey, String mdcValue) throws IOException {
33+
if (shouldParse(mdcValue)) {
34+
try {
35+
double parsedValue = Double.parseDouble(mdcValue);
36+
generator.writeFieldName(fieldName);
37+
generator.writeNumber(parsedValue);
38+
return true;
39+
} catch (NumberFormatException ignore) {
40+
}
41+
}
42+
43+
return false;
44+
}
45+
46+
/**
47+
* Returns true if an attempt at parsing the given value should be made.
48+
* When true is returned, we can be reasonably confident that {@link Double#parseDouble(String)}
49+
* will succeed. However, it is not guaranteed to succeed.
50+
* This is mainly to avoid throwing/catching {@link NumberFormatException}
51+
* in as many cases as possible.
52+
*/
53+
private boolean shouldParse(String value) {
54+
return value != null && !value.isEmpty() && DOUBLE_PATTERN.matcher(value).matches();
55+
}
56+
57+
/**
58+
* Returns a Pattern that matches strings that can be parsed by {@link Double#parseDouble(String)}.
59+
* This regex comes from the javadoc for {@link Double#valueOf(String)},
60+
* but with NaN and Infinity removed.
61+
*/
62+
private static Pattern doublePattern() {
63+
final String Digits = "(\\p{Digit}+)";
64+
final String HexDigits = "(\\p{XDigit}+)";
65+
// an exponent is 'e' or 'E' followed by an optionally
66+
// signed decimal integer.
67+
final String Exp = "[eE][+-]?" + Digits;
68+
final String fpRegex =
69+
("[\\x00-\\x20]*" // Optional leading "whitespace"
70+
+ "[+-]?(" // Optional sign character
71+
72+
// A decimal floating-point string representing a finite positive
73+
// number without a leading sign has at most five basic pieces:
74+
// Digits . Digits ExponentPart FloatTypeSuffix
75+
//
76+
// Since this method allows integer-only strings as input
77+
// in addition to strings of floating-point literals, the
78+
// two sub-patterns below are simplifications of the grammar
79+
// productions from section 3.10.2 of
80+
// The Java Language Specification.
81+
82+
// Digits ._opt Digits_opt ExponentPart_opt FloatTypeSuffix_opt
83+
+ "(((" + Digits + "(\\.)?(" + Digits + "?)(" + Exp + ")?)|"
84+
85+
// . Digits ExponentPart_opt FloatTypeSuffix_opt
86+
+ "(\\.(" + Digits + ")(" + Exp + ")?)|"
87+
88+
// Hexadecimal strings
89+
+ "(("
90+
// 0[xX] HexDigits ._opt BinaryExponent FloatTypeSuffix_opt
91+
+ "(0[xX]" + HexDigits + "(\\.)?)|"
92+
93+
// 0[xX] HexDigits_opt . HexDigits BinaryExponent FloatTypeSuffix_opt
94+
+ "(0[xX]" + HexDigits + "?(\\.)" + HexDigits + ")"
95+
96+
+ ")[pP][+-]?" + Digits + "))"
97+
+ "[fFdD]?))"
98+
+ "[\\x00-\\x20]*"); // Optional trailing "whitespace"
99+
return Pattern.compile(fpRegex);
100+
}
101+
102+
}

0 commit comments

Comments
 (0)