Skip to content

Commit

Permalink
Add MDC entry writers to be able to convert values (#957)
Browse files Browse the repository at this point in the history
  • Loading branch information
jug committed Jun 18, 2023
1 parent d23b70d commit 568466b
Show file tree
Hide file tree
Showing 16 changed files with 668 additions and 6 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,51 @@ specify `<mdcKeyFieldName>mdcKeyName=fieldName</mdcKeyFieldName>`:
</encoder>
```

You can also manipulate the MDC entry values written to the JSON output.
By default, no manipulations are done and all MDC entry values are written as text.

Currently, MDC entry writers for the following value types are supported:

```xml
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!--
Writes long values (instead of String values) for any MDC values
that can be parsed as a long (radix 10).
e.g. Writes 1234 instead of "1234"
-->
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.LongMdcEntryWriter"/>

<!--
Writes double values (instead of String values) for any MDC values
that can be parsed as a double, except NaN and positive/negative Infinity.
e.g. 1234.5678 instead of "1234.5678"
-->
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.DoubleMdcEntryWriter"/>

<!--
Writes boolean values (instead of String values) for any MDC values
that equal "true" or "false", ignoring case.
e.g. Writes true instead of "true"
-->
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.BooleanMdcEntryWriter"/>
</encoder>
```

To add your own MDC entry writer for other types or apply the manipulations only for specific fields
you can write your own implementation of [`MdcEntryWriter`](src/main/java/net/logstash/logback/composite/loggingevent/mdc/MdcEntryWriter.java).

You can also replace the default MDC JSON provider with your own class extending from
[`MdcJsonProvider`](src/main/java/net/logstash/logback/composite/loggingevent/MdcJsonProvider.java).
Configuring your class as a [Custom JSON Provider](#custom-json-provider) will then replace
the default `MdcJsonProvider`.

```xml
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<provider class="mypackagenames.MyCustomMdcJsonProvider"/>
</encoder>
```


### 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.
Expand Down Expand Up @@ -1071,6 +1116,7 @@ specify`<keyValuePairsKeyFieldName>keyName=fieldName</keyValuePairsKeyFieldName>
</encoder>
```


### Context fields

By default, each property of Logback's Context (`ch.qos.logback.core.Context`)
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/net/logstash/logback/LogstashFormatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import net.logstash.logback.composite.loggingevent.MessageJsonProvider;
import net.logstash.logback.composite.loggingevent.StackTraceJsonProvider;
import net.logstash.logback.composite.loggingevent.TagsJsonProvider;
import net.logstash.logback.composite.loggingevent.mdc.MdcEntryWriter;
import net.logstash.logback.fieldnames.LogstashFieldNames;

import ch.qos.logback.classic.pattern.ThrowableHandlingConverter;
Expand Down Expand Up @@ -322,6 +323,16 @@ public void addMdcKeyFieldName(String mdcKeyFieldName) {
mdcProvider.addMdcKeyFieldName(mdcKeyFieldName);
}
}
public List<MdcEntryWriter> getMdcEntryWriters() {
return isIncludeMdc()
? mdcProvider.getMdcEntryWriters()
: Collections.emptyList();
}
public void addMdcEntryWriter(MdcEntryWriter mdcEntryWriter) {
if (isIncludeMdc()) {
mdcProvider.addMdcEntryWriter(mdcEntryWriter);
}
}

public List<String> getIncludeKeyValueKeyNames() {
return isIncludeKeyValuePairs()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import net.logstash.logback.composite.AbstractFieldJsonProvider;
import net.logstash.logback.composite.FieldNamesAware;
import net.logstash.logback.composite.loggingevent.mdc.MdcEntryWriter;
import net.logstash.logback.fieldnames.LogstashFieldNames;

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

/**
* See {@link MdcJsonProvider}.
*/
private List<String> includeMdcKeyNames = new ArrayList<>();
protected List<String> includeMdcKeyNames = new ArrayList<>();

/**
* See {@link MdcJsonProvider}.
*/
private List<String> excludeMdcKeyNames = new ArrayList<>();
protected List<String> excludeMdcKeyNames = new ArrayList<>();

protected final Map<String, String> mdcKeyFieldNames = new HashMap<>();

private final Map<String, String> mdcKeyFieldNames = new HashMap<>();
/**
* See {@link MdcJsonProvider}.
*/
protected final List<MdcEntryWriter> mdcEntryWriters = new ArrayList<>();

@Override
public void start() {
Expand Down Expand Up @@ -101,8 +110,7 @@ public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOExcep
generator.writeObjectFieldStart(getFieldName());
hasWrittenStart = true;
}
generator.writeFieldName(fieldName);
generator.writeObject(entry.getValue());
writeMdcEntry(generator, fieldName, entry.getKey(), entry.getValue());
}
}
if (hasWrittenStart) {
Expand All @@ -119,19 +127,23 @@ public void setFieldNames(LogstashFieldNames fieldNames) {
public List<String> getIncludeMdcKeyNames() {
return Collections.unmodifiableList(includeMdcKeyNames);
}

public void addIncludeMdcKeyName(String includedMdcKeyName) {
this.includeMdcKeyNames.add(includedMdcKeyName);
}

public void setIncludeMdcKeyNames(List<String> includeMdcKeyNames) {
this.includeMdcKeyNames = new ArrayList<String>(includeMdcKeyNames);
this.includeMdcKeyNames = new ArrayList<>(includeMdcKeyNames);
}

public List<String> getExcludeMdcKeyNames() {
return Collections.unmodifiableList(excludeMdcKeyNames);
}

public void addExcludeMdcKeyName(String excludedMdcKeyName) {
this.excludeMdcKeyNames.add(excludedMdcKeyName);
}

public void setExcludeMdcKeyNames(List<String> excludeMdcKeyNames) {
this.excludeMdcKeyNames = new ArrayList<>(excludeMdcKeyNames);
}
Expand All @@ -140,6 +152,13 @@ public Map<String, String> getMdcKeyFieldNames() {
return mdcKeyFieldNames;
}

public List<MdcEntryWriter> getMdcEntryWriters() {
return Collections.unmodifiableList(mdcEntryWriters);
}
public void addMdcEntryWriter(MdcEntryWriter mdcEntryWriter) {
this.mdcEntryWriters.add(mdcEntryWriter);
}

/**
* Adds the given mdcKeyFieldName entry in the form mdcKeyName=fieldName
* to use an alternative field name for an MDC key.
Expand All @@ -154,4 +173,26 @@ public void addMdcKeyFieldName(String mdcKeyFieldName) {
mdcKeyFieldNames.put(split[0], split[1]);
}

/**
* Writes the MDC entry with the given generator by iterating over the chain of {@link #mdcEntryWriters}
* in the given order till the first {@link MdcEntryWriter} returns true.
* <p>
* If none of the {@link #mdcEntryWriters} returned true, the MDC field is written as String value by default.
*
* @param generator the generator to write the entry to.
* @param fieldName the field name to use when writing the entry.
* @param mdcKey the key of the MDC map entry.
* @param mdcValue the value of the MDC map entry.
*/
private void writeMdcEntry(JsonGenerator generator, String fieldName, String mdcKey, String mdcValue) throws IOException {
for (MdcEntryWriter mdcEntryWriter : this.mdcEntryWriters) {
if (mdcEntryWriter.writeMdcEntry(generator, fieldName, mdcKey, mdcValue)) {
return;
}
}

generator.writeFieldName(fieldName);
generator.writeObject(mdcValue);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.mdc;

import java.io.IOException;

import com.fasterxml.jackson.core.JsonGenerator;

/**
* Writes boolean values (instead of String values) for any MDC values that equal "true" or "false", ignoring case.
*/
public class BooleanMdcEntryWriter implements MdcEntryWriter {

@Override
public boolean writeMdcEntry(JsonGenerator generator, String fieldName, String mdcKey, String mdcValue) throws IOException {
if ("true".equalsIgnoreCase(mdcValue)) {
generator.writeFieldName(fieldName);
generator.writeBoolean(true);
return true;
}
if ("false".equalsIgnoreCase(mdcValue)) {
generator.writeFieldName(fieldName);
generator.writeBoolean(false);
return true;
}

return false;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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.mdc;

import java.io.IOException;
import java.util.regex.Pattern;

import com.fasterxml.jackson.core.JsonGenerator;

/**
* Writes double values (instead of String values) for any MDC values that can be parsed as a double,
* except NaN and positive/negative Infinity.
*/
public class DoubleMdcEntryWriter implements MdcEntryWriter {

private static final Pattern DOUBLE_PATTERN = doublePattern();

@Override
public boolean writeMdcEntry(JsonGenerator generator, String fieldName, String mdcKey, String mdcValue) throws IOException {
if (shouldParse(mdcValue)) {
try {
double parsedValue = Double.parseDouble(mdcValue);
generator.writeFieldName(fieldName);
generator.writeNumber(parsedValue);
return true;
} catch (NumberFormatException ignore) {
}
}

return false;
}

/**
* Returns true if an attempt at parsing the given value should be made.
* When true is returned, we can be reasonably confident that {@link Double#parseDouble(String)}
* will succeed. However, it is not guaranteed to succeed.
* This is mainly to avoid throwing/catching {@link NumberFormatException}
* in as many cases as possible.
*/
private boolean shouldParse(String value) {
return value != null && !value.isEmpty() && DOUBLE_PATTERN.matcher(value).matches();
}

/**
* Returns a Pattern that matches strings that can be parsed by {@link Double#parseDouble(String)}.
* This regex comes from the javadoc for {@link Double#valueOf(String)},
* but with NaN and Infinity removed.
*/
private static Pattern doublePattern() {
final String Digits = "(\\p{Digit}+)";
final String HexDigits = "(\\p{XDigit}+)";
// an exponent is 'e' or 'E' followed by an optionally
// signed decimal integer.
final String Exp = "[eE][+-]?" + Digits;
final String fpRegex =
("[\\x00-\\x20]*" // Optional leading "whitespace"
+ "[+-]?(" // Optional sign character

// A decimal floating-point string representing a finite positive
// number without a leading sign has at most five basic pieces:
// Digits . Digits ExponentPart FloatTypeSuffix
//
// Since this method allows integer-only strings as input
// in addition to strings of floating-point literals, the
// two sub-patterns below are simplifications of the grammar
// productions from section 3.10.2 of
// The Java Language Specification.

// Digits ._opt Digits_opt ExponentPart_opt FloatTypeSuffix_opt
+ "(((" + Digits + "(\\.)?(" + Digits + "?)(" + Exp + ")?)|"

// . Digits ExponentPart_opt FloatTypeSuffix_opt
+ "(\\.(" + Digits + ")(" + Exp + ")?)|"

// Hexadecimal strings
+ "(("
// 0[xX] HexDigits ._opt BinaryExponent FloatTypeSuffix_opt
+ "(0[xX]" + HexDigits + "(\\.)?)|"

// 0[xX] HexDigits_opt . HexDigits BinaryExponent FloatTypeSuffix_opt
+ "(0[xX]" + HexDigits + "?(\\.)" + HexDigits + ")"

+ ")[pP][+-]?" + Digits + "))"
+ "[fFdD]?))"
+ "[\\x00-\\x20]*"); // Optional trailing "whitespace"
return Pattern.compile(fpRegex);
}

}
Loading

0 comments on commit 568466b

Please sign in to comment.