Skip to content

Commit 94ccc40

Browse files
authored
Add composite MdcEntryWriter to filter MDC keys with regex (#974)
1 parent 05c40f7 commit 94ccc40

File tree

5 files changed

+219
-2
lines changed

5 files changed

+219
-2
lines changed

README.md

+18-1
Original file line numberDiff line numberDiff line change
@@ -1033,7 +1033,7 @@ specify `<mdcKeyFieldName>mdcKeyName=fieldName</mdcKeyFieldName>`:
10331033
You can also manipulate the MDC entry values written to the JSON output.
10341034
By default, no manipulations are done and all MDC entry values are written as text.
10351035

1036-
Currently, MDC entry writers for the following value types are supported:
1036+
Currently, the following MDC entry writers are supported:
10371037

10381038
```xml
10391039
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
@@ -1057,6 +1057,23 @@ Currently, MDC entry writers for the following value types are supported:
10571057
e.g. Writes true instead of "true"
10581058
-->
10591059
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.BooleanMdcEntryWriter"/>
1060+
1061+
<!--
1062+
Composite MDC entry writer that delegates writing MDC entries to a list of `MdcEntryWriter`
1063+
if the key of an MDC entry does match a given include pattern and does not match a given
1064+
exclude pattern.
1065+
1066+
Omitting the 'includeMdcKeyPattern' means to include all MDC keys.
1067+
Omitting the 'excludeMdcKeyPattern' means to exclude no MDC keys.
1068+
1069+
The elements with the key patterns are optional. If provided, use a regular expression
1070+
with the syntax of `java.util.regex.Pattern`.
1071+
-->
1072+
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.RegexFilteringMdcEntryWriter">
1073+
<includeMdcKeyPattern>keyPatternToInclude</includeMdcKeyPattern>
1074+
<excludeMdcKeyPattern>keyPatternToExclude</excludeMdcKeyPattern>
1075+
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.LongMdcEntryWriter"/>
1076+
</mdcEntryWriter>
10601077
</encoder>
10611078
```
10621079

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2013-2023 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.ArrayList;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.regex.Pattern;
23+
24+
import com.fasterxml.jackson.core.JsonGenerator;
25+
26+
/**
27+
* Writes MDC entries by delegating to other instances of {@link MdcEntryWriter} if MDC key matches the given include
28+
* and exclude pattern.
29+
*
30+
* <ol>
31+
* <li>An MDC entry is written if the MDC key does match the {@link #includeMdcKeyPattern} AND does not match
32+
* the {@link #excludeMdcKeyPattern}.</li>
33+
* <li>Omitting a {@link #includeMdcKeyPattern} means to include all MDC keys.</li>
34+
* <li>Omitting a {@link #excludeMdcKeyPattern} means to exclude no MDC keys.</li>
35+
* </ol>
36+
*/
37+
public class RegexFilteringMdcEntryWriter implements MdcEntryWriter {
38+
39+
private Pattern includeMdcKeyPattern;
40+
private Pattern excludeMdcKeyPattern;
41+
private final List<MdcEntryWriter> mdcEntryWriters = new ArrayList<>();
42+
43+
@Override
44+
public boolean writeMdcEntry(JsonGenerator generator, String fieldName, String mdcKey, String mdcValue) throws IOException {
45+
if (shouldWrite(mdcKey)) {
46+
for (MdcEntryWriter mdcEntryWriter : this.mdcEntryWriters) {
47+
if (mdcEntryWriter.writeMdcEntry(generator, fieldName, mdcKey, mdcValue)) {
48+
return true;
49+
}
50+
}
51+
}
52+
53+
return false;
54+
}
55+
56+
public Pattern getIncludeMdcKeyPattern() {
57+
return includeMdcKeyPattern;
58+
}
59+
public void setIncludeMdcKeyPattern(String includeMdcKeyPattern) {
60+
this.includeMdcKeyPattern = Pattern.compile(includeMdcKeyPattern);
61+
}
62+
63+
public Pattern getExcludeMdcKeyPattern() {
64+
return excludeMdcKeyPattern;
65+
}
66+
public void setExcludeMdcKeyPattern(String excludeMdcKeyPattern) {
67+
this.excludeMdcKeyPattern = Pattern.compile(excludeMdcKeyPattern);
68+
}
69+
70+
public List<MdcEntryWriter> getMdcEntryWriters() {
71+
return Collections.unmodifiableList(mdcEntryWriters);
72+
}
73+
public void addMdcEntryWriter(MdcEntryWriter mdcEntryWriter) {
74+
this.mdcEntryWriters.add(mdcEntryWriter);
75+
}
76+
77+
/** Returns true if passed MDC key should be written to the JSON output. */
78+
private boolean shouldWrite(String key) {
79+
if (this.mdcEntryWriters.isEmpty()) {
80+
return false;
81+
}
82+
boolean includeKey = includeMdcKeyPattern == null || includeMdcKeyPattern.matcher(key).matches();
83+
boolean excludeKey = excludeMdcKeyPattern != null && excludeMdcKeyPattern.matcher(key).matches();
84+
return includeKey && !excludeKey;
85+
}
86+
87+
}

src/test/java/net/logstash/logback/ConfigurationTest.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@
5252
import net.logstash.logback.composite.loggingevent.SequenceJsonProvider;
5353
import net.logstash.logback.composite.loggingevent.StackTraceJsonProvider;
5454
import net.logstash.logback.composite.loggingevent.TagsJsonProvider;
55+
import net.logstash.logback.composite.loggingevent.mdc.BooleanMdcEntryWriter;
5556
import net.logstash.logback.composite.loggingevent.mdc.LongMdcEntryWriter;
57+
import net.logstash.logback.composite.loggingevent.mdc.RegexFilteringMdcEntryWriter;
5658
import net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder;
5759
import net.logstash.logback.marker.Markers;
5860
import net.logstash.logback.stacktrace.ShortenedThrowableConverter;
@@ -180,8 +182,17 @@ private void verifyCommonProviders(List<JsonProvider<ILoggingEvent>> providers)
180182
assertThat(mdcJsonProvider).isNotNull();
181183
assertThat(mdcJsonProvider.getIncludeMdcKeyNames()).containsExactly("included");
182184
assertThat(mdcJsonProvider.getMdcKeyFieldNames()).containsOnly(entry("key", "renamedKey"));
183-
assertThat(mdcJsonProvider.getMdcEntryWriters()).hasSize(1);
185+
assertThat(mdcJsonProvider.getMdcEntryWriters()).hasSize(2);
184186
assertThat(mdcJsonProvider.getMdcEntryWriters()).element(0).isExactlyInstanceOf(LongMdcEntryWriter.class);
187+
assertThat(mdcJsonProvider.getMdcEntryWriters()).element(1).isExactlyInstanceOf(RegexFilteringMdcEntryWriter.class);
188+
RegexFilteringMdcEntryWriter regexFilteringMdcEntryWriter =
189+
(RegexFilteringMdcEntryWriter) mdcJsonProvider.getMdcEntryWriters().get(1);
190+
assertThat(regexFilteringMdcEntryWriter.getIncludeMdcKeyPattern()).isNotNull();
191+
assertThat("include").matches(regexFilteringMdcEntryWriter.getIncludeMdcKeyPattern());
192+
assertThat(regexFilteringMdcEntryWriter.getExcludeMdcKeyPattern()).isNotNull();
193+
assertThat("exclude").matches(regexFilteringMdcEntryWriter.getExcludeMdcKeyPattern());
194+
assertThat(regexFilteringMdcEntryWriter.getMdcEntryWriters()).hasSize(1);
195+
assertThat(regexFilteringMdcEntryWriter.getMdcEntryWriters()).element(0).isExactlyInstanceOf(BooleanMdcEntryWriter.class);
185196

186197
KeyValuePairsJsonProvider keyValuePairsJsonProvider = getInstance(providers, KeyValuePairsJsonProvider.class);
187198
assertThat(keyValuePairsJsonProvider).isNotNull();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2013-2023 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 static org.assertj.core.api.Assertions.assertThat;
19+
import static org.mockito.ArgumentMatchers.anyString;
20+
import static org.mockito.ArgumentMatchers.eq;
21+
import static org.mockito.Mockito.verify;
22+
import static org.mockito.Mockito.verifyNoMoreInteractions;
23+
import static org.mockito.Mockito.when;
24+
25+
import java.io.IOException;
26+
27+
import com.fasterxml.jackson.core.JsonGenerator;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.extension.ExtendWith;
30+
import org.junit.jupiter.params.ParameterizedTest;
31+
import org.junit.jupiter.params.provider.CsvSource;
32+
import org.mockito.Mock;
33+
import org.mockito.junit.jupiter.MockitoExtension;
34+
35+
@ExtendWith(MockitoExtension.class)
36+
class RegexFilteringMdcEntryWriterTest {
37+
38+
private final RegexFilteringMdcEntryWriter mdcEntryWriter = new RegexFilteringMdcEntryWriter();
39+
40+
@Mock
41+
private JsonGenerator generator;
42+
@Mock
43+
private MdcEntryWriter mockedMdcEntryWriter;
44+
45+
@Test
46+
void noIncludeAndNoExcludePattern() throws IOException {
47+
mockMdcEntryWriter();
48+
49+
boolean result = mdcEntryWriter.writeMdcEntry(generator, "field", "key", "value");
50+
51+
assertThat(result).isTrue();
52+
verify(mockedMdcEntryWriter).writeMdcEntry(generator, "field", "key", "value");
53+
verifyNoMoreInteractions(mockedMdcEntryWriter);
54+
}
55+
56+
@Test
57+
void noMdcEntryWriter() throws IOException {
58+
boolean result = mdcEntryWriter.writeMdcEntry(generator, "field", "key", "value");
59+
60+
assertThat(result).isFalse();
61+
verifyNoMoreInteractions(mockedMdcEntryWriter);
62+
}
63+
64+
@ParameterizedTest
65+
@CsvSource({
66+
"include,excl.*,true",
67+
"include,.*lude,false",
68+
"exclude,excl.*,false",
69+
"other,excl.*,false"
70+
})
71+
void includeAndExcludePattern(String mdcKey, String excludePattern, boolean entryWritten) throws IOException {
72+
if (entryWritten) {
73+
mockMdcEntryWriter();
74+
}
75+
mdcEntryWriter.setIncludeMdcKeyPattern("incl.*");
76+
mdcEntryWriter.setExcludeMdcKeyPattern(excludePattern);
77+
78+
boolean result = mdcEntryWriter.writeMdcEntry(generator, "field", mdcKey, "value");
79+
80+
assertThat(result).isEqualTo(entryWritten);
81+
if (entryWritten) {
82+
verify(mockedMdcEntryWriter).writeMdcEntry(generator, "field", mdcKey, "value");
83+
}
84+
verifyNoMoreInteractions(mockedMdcEntryWriter);
85+
}
86+
87+
private void mockMdcEntryWriter() throws IOException {
88+
when(mockedMdcEntryWriter.writeMdcEntry(eq(generator), anyString(), anyString(), anyString())).thenReturn(true);
89+
mdcEntryWriter.addMdcEntryWriter(mockedMdcEntryWriter);
90+
}
91+
92+
}

src/test/resources/logback-test.xml

+10
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
<includeMdcKeyName>included</includeMdcKeyName>
5252
<mdcKeyFieldName>key=renamedKey</mdcKeyFieldName>
5353
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.LongMdcEntryWriter"/>
54+
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.RegexFilteringMdcEntryWriter">
55+
<includeMdcKeyPattern>incl.*</includeMdcKeyPattern>
56+
<excludeMdcKeyPattern>excl.*</excludeMdcKeyPattern>
57+
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.BooleanMdcEntryWriter"/>
58+
</mdcEntryWriter>
5459
<includeKeyValueKeyName>included</includeKeyValueKeyName>
5560
<keyValueKeyFieldName>key=renamedKey</keyValueKeyFieldName>
5661
<customFields>{"customName":"customValue"}</customFields>
@@ -142,6 +147,11 @@
142147
<includeMdcKeyName>included</includeMdcKeyName>
143148
<mdcKeyFieldName>key=renamedKey</mdcKeyFieldName>
144149
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.LongMdcEntryWriter"/>
150+
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.RegexFilteringMdcEntryWriter">
151+
<includeMdcKeyPattern>incl.*</includeMdcKeyPattern>
152+
<excludeMdcKeyPattern>excl.*</excludeMdcKeyPattern>
153+
<mdcEntryWriter class="net.logstash.logback.composite.loggingevent.mdc.BooleanMdcEntryWriter"/>
154+
</mdcEntryWriter>
145155
</mdc>
146156
<keyValuePairs>
147157
<includeKeyName>included</includeKeyName>

0 commit comments

Comments
 (0)