Skip to content

Commit 7ee13d8

Browse files
feat: support customization for MDC serializer
1 parent 7ff2b43 commit 7ee13d8

File tree

7 files changed

+260
-62
lines changed

7 files changed

+260
-62
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*-
2+
* #%L
3+
* Java ECS logging
4+
* %%
5+
* Copyright (C) 2019 - 2020 Elastic and contributors
6+
* %%
7+
* Licensed to Elasticsearch B.V. under one or more contributor
8+
* license agreements. See the NOTICE file distributed with
9+
* this work for additional information regarding copyright
10+
* ownership. Elasticsearch B.V. licenses this file to you under
11+
* the Apache License, Version 2.0 (the "License"); you may
12+
* not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing,
18+
* software distributed under the License is distributed on an
19+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20+
* KIND, either express or implied. See the License for the
21+
* specific language governing permissions and limitations
22+
* under the License.
23+
* #L%
24+
*/
25+
package co.elastic.logging.log4j2;
26+
27+
import co.elastic.logging.EcsJsonSerializer;
28+
import co.elastic.logging.JsonUtils;
29+
import org.apache.logging.log4j.core.LogEvent;
30+
import org.apache.logging.log4j.util.TriConsumer;
31+
32+
interface DefaultMdcSerializer extends MdcSerializer {
33+
34+
/**
35+
* Garbage free MDC serialization for log4j2 2.7+
36+
* Never reference directly in prod code so avoid linkage errors when TriConsumer or getContextData are not available
37+
*/
38+
enum UsingContextData implements MdcSerializer {
39+
40+
@SuppressWarnings("unused")
41+
INSTANCE;
42+
43+
private static final TriConsumer<String, Object, StringBuilder> WRITE_MDC = new TriConsumer<String, Object, StringBuilder>() {
44+
@Override
45+
public void accept(final String key, final Object value, final StringBuilder stringBuilder) {
46+
stringBuilder.append('\"');
47+
JsonUtils.quoteAsString(key, stringBuilder);
48+
stringBuilder.append("\":\"");
49+
JsonUtils.quoteAsString(EcsJsonSerializer.toNullSafeString(String.valueOf(value)), stringBuilder);
50+
stringBuilder.append("\",");
51+
}
52+
};
53+
54+
55+
@Override
56+
public void serializeMdc(LogEvent event, StringBuilder builder) {
57+
event.getContextData().forEach(WRITE_MDC, builder);
58+
}
59+
}
60+
61+
/**
62+
* Fallback for log4j2 <= 2.6
63+
*/
64+
enum UsingContextMap implements MdcSerializer {
65+
INSTANCE;
66+
67+
@Override
68+
public void serializeMdc(LogEvent event, StringBuilder builder) {
69+
EcsJsonSerializer.serializeMDC(builder, event.getContextMap());
70+
}
71+
}
72+
}

log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java

+16-4
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ public class EcsLayout extends AbstractStringLayout {
6262

6363
public static final Charset UTF_8 = Charset.forName("UTF-8");
6464
private static final ObjectMessageJacksonSerializer JACKSON_SERIALIZER = ObjectMessageJacksonSerializer.Resolver.resolve();
65-
private static final MdcSerializer MDC_SERIALIZER = MdcSerializer.Resolver.resolve();
6665
private static final MultiFormatHandler MULTI_FORMAT_HANDLER = MultiFormatHandler.Resolver.resolve();
6766
private static final boolean FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS = PropertiesUtil.getProperties().getBooleanProperty(
6867
"log4j2.formatMsgNoLookups", false);
@@ -79,9 +78,10 @@ public class EcsLayout extends AbstractStringLayout {
7978
private final boolean includeOrigin;
8079
private final PatternFormatter[] exceptionPatternFormatter;
8180
private final ConcurrentMap<Class<? extends MultiformatMessage>, Boolean> supportsJson = new ConcurrentHashMap<Class<? extends MultiformatMessage>, Boolean>();
81+
private final MdcSerializer mdcSerializer;
8282

8383
private EcsLayout(Configuration config, String serviceName, String serviceVersion, String serviceEnvironment, String serviceNodeName, String eventDataset, boolean includeMarkers,
84-
KeyValuePair[] additionalFields, boolean includeOrigin, String exceptionPattern, boolean stackTraceAsArray) {
84+
KeyValuePair[] additionalFields, boolean includeOrigin, String exceptionPattern, boolean stackTraceAsArray, String mdcSerializerFullClassName) {
8585
super(config, UTF_8, null, null);
8686
this.serviceName = serviceName;
8787
this.serviceVersion = serviceVersion;
@@ -109,6 +109,7 @@ private EcsLayout(Configuration config, String serviceName, String serviceVersio
109109
} else {
110110
exceptionPatternFormatter = null;
111111
}
112+
mdcSerializer = MdcSerializerResolver.resolve(mdcSerializerFullClassName);
112113
}
113114

114115
@PluginBuilderFactory
@@ -193,7 +194,7 @@ private void serializeAdditionalFieldsAndMDC(LogEvent event, StringBuilder build
193194
}
194195
}
195196
}
196-
MDC_SERIALIZER.serializeMdc(event, builder);
197+
mdcSerializer.serializeMdc(event, builder);
197198
}
198199

199200
private static void formatPattern(LogEvent event, PatternFormatter[] formatters, StringBuilder buffer) {
@@ -377,6 +378,8 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde
377378
private KeyValuePair[] additionalFields = new KeyValuePair[]{};
378379
@PluginBuilderAttribute("includeOrigin")
379380
private boolean includeOrigin = false;
381+
@PluginBuilderAttribute("mdcSerializer")
382+
private String mdcSerializerFullClassName = "";
380383

381384
Builder() {
382385
}
@@ -428,6 +431,10 @@ public String getExceptionPattern() {
428431
return exceptionPattern;
429432
}
430433

434+
public String getMdcSerializerFullClassName() {
435+
return mdcSerializerFullClassName;
436+
}
437+
431438
/**
432439
* Additional fields to set on each log event.
433440
*
@@ -483,11 +490,16 @@ public EcsLayout.Builder setExceptionPattern(String exceptionPattern) {
483490
return this;
484491
}
485492

493+
public EcsLayout.Builder setMdcSerializerFullClassName(String mdcSerializerFullClassName) {
494+
this.mdcSerializerFullClassName = mdcSerializerFullClassName;
495+
return this;
496+
}
497+
486498
@Override
487499
public EcsLayout build() {
488500
return new EcsLayout(getConfiguration(), serviceName, serviceVersion, serviceEnvironment, serviceNodeName,
489501
EcsJsonSerializer.computeEventDataset(eventDataset, serviceName),
490-
includeMarkers, additionalFields, includeOrigin, exceptionPattern, stackTraceAsArray);
502+
includeMarkers, additionalFields, includeOrigin, exceptionPattern, stackTraceAsArray, mdcSerializerFullClassName);
491503
}
492504
}
493505
}

log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/MdcSerializer.java

+7-58
Original file line numberDiff line numberDiff line change
@@ -24,65 +24,14 @@
2424
*/
2525
package co.elastic.logging.log4j2;
2626

27-
import co.elastic.logging.EcsJsonSerializer;
28-
import co.elastic.logging.JsonUtils;
2927
import org.apache.logging.log4j.core.LogEvent;
30-
import org.apache.logging.log4j.util.TriConsumer;
31-
32-
interface MdcSerializer {
3328

29+
/**
30+
* Interface for serializing MDC (Mapped Diagnostic Context) data from a {@link LogEvent}.
31+
* <p>
32+
* Implementations must have a public no-argument constructor to allow dynamic instantiation.
33+
* </p>
34+
*/
35+
public interface MdcSerializer {
3436
void serializeMdc(LogEvent event, StringBuilder builder);
35-
36-
class Resolver {
37-
38-
public static MdcSerializer resolve() {
39-
try {
40-
LogEvent.class.getMethod("getContextData");
41-
return (MdcSerializer) Class.forName("co.elastic.logging.log4j2.MdcSerializer$UsingContextData").getEnumConstants()[0];
42-
} catch (Exception ignore) {
43-
} catch (LinkageError ignore) {
44-
}
45-
return UsingContextMap.INSTANCE;
46-
}
47-
48-
}
49-
50-
/**
51-
* Garbage free MDC serialization for log4j2 2.7+
52-
* Never reference directly in prod code so avoid linkage errors when TriConsumer or getContextData are not available
53-
*/
54-
enum UsingContextData implements MdcSerializer {
55-
56-
@SuppressWarnings("unused")
57-
INSTANCE;
58-
59-
private static final TriConsumer<String, Object, StringBuilder> WRITE_MDC = new TriConsumer<String, Object, StringBuilder>() {
60-
@Override
61-
public void accept(final String key, final Object value, final StringBuilder stringBuilder) {
62-
stringBuilder.append('\"');
63-
JsonUtils.quoteAsString(key, stringBuilder);
64-
stringBuilder.append("\":\"");
65-
JsonUtils.quoteAsString(EcsJsonSerializer.toNullSafeString(String.valueOf(value)), stringBuilder);
66-
stringBuilder.append("\",");
67-
}
68-
};
69-
70-
71-
@Override
72-
public void serializeMdc(LogEvent event, StringBuilder builder) {
73-
event.getContextData().forEach(WRITE_MDC, builder);
74-
}
75-
}
76-
77-
/**
78-
* Fallback for log4j2 <= 2.6
79-
*/
80-
enum UsingContextMap implements MdcSerializer {
81-
INSTANCE;
82-
83-
@Override
84-
public void serializeMdc(LogEvent event, StringBuilder builder) {
85-
EcsJsonSerializer.serializeMDC(builder, event.getContextMap());
86-
}
87-
}
8837
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*-
2+
* #%L
3+
* Java ECS logging
4+
* %%
5+
* Copyright (C) 2019 - 2020 Elastic and contributors
6+
* %%
7+
* Licensed to Elasticsearch B.V. under one or more contributor
8+
* license agreements. See the NOTICE file distributed with
9+
* this work for additional information regarding copyright
10+
* ownership. Elasticsearch B.V. licenses this file to you under
11+
* the Apache License, Version 2.0 (the "License"); you may
12+
* not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing,
18+
* software distributed under the License is distributed on an
19+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20+
* KIND, either express or implied. See the License for the
21+
* specific language governing permissions and limitations
22+
* under the License.
23+
* #L%
24+
*/
25+
package co.elastic.logging.log4j2;
26+
27+
import java.lang.reflect.InvocationTargetException;
28+
29+
import org.apache.logging.log4j.core.LogEvent;
30+
31+
import co.elastic.logging.log4j2.DefaultMdcSerializer.UsingContextMap;
32+
33+
interface MdcSerializerResolver {
34+
35+
static MdcSerializer resolve(String mdcSerializerFullClassName) {
36+
if (mdcSerializerFullClassName == null || mdcSerializerFullClassName.isEmpty()) {
37+
return resolveDefault();
38+
}
39+
try {
40+
Class<?> clazz = Class.forName(mdcSerializerFullClassName);
41+
return (MdcSerializer) clazz.getDeclaredConstructor().newInstance();
42+
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException
43+
| NoSuchMethodException | InvocationTargetException e) {
44+
return resolveDefault();
45+
}
46+
}
47+
48+
private static MdcSerializer resolveDefault() {
49+
try {
50+
LogEvent.class.getMethod("getContextData");
51+
return (DefaultMdcSerializer) Class.forName(
52+
"co.elastic.logging.log4j2.DefaultMdcSerializer$UsingContextData").getEnumConstants()[0];
53+
} catch (Exception | LinkageError ignore) {
54+
}
55+
return UsingContextMap.INSTANCE;
56+
}
57+
58+
}

log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/AbstractLog4j2EcsLayoutTest.java

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
import java.util.List;
3939

40+
import static co.elastic.logging.log4j2.CustomMdcSerializer.CUSTOM_MDC_SERIALIZER_TEST_KEY;
4041
import static org.assertj.core.api.Assertions.assertThat;
4142

4243
abstract class AbstractLog4j2EcsLayoutTest extends AbstractEcsLoggingTest {
@@ -52,6 +53,7 @@ void tearDown() throws Exception {
5253
void testAdditionalFieldsWithLookup() throws Exception {
5354
putMdc("trace.id", "foo");
5455
putMdc("foo", "bar");
56+
putMdc(CUSTOM_MDC_SERIALIZER_TEST_KEY, "some_text_lower_case");
5557
debug("test");
5658
assertThat(getAndValidateLastLogLine().get("cluster.uuid").textValue()).isEqualTo("9fe9134b-20b0-465e-acf9-8cc09ac9053b");
5759
assertThat(getAndValidateLastLogLine().get("node.id").textValue()).isEqualTo("foo");
@@ -60,6 +62,7 @@ void testAdditionalFieldsWithLookup() throws Exception {
6062
assertThat(getAndValidateLastLogLine().get("clazz").textValue()).startsWith(getClass().getPackageName());
6163
assertThat(getAndValidateLastLogLine().get("404")).isNull();
6264
assertThat(getAndValidateLastLogLine().get("foo").textValue()).isEqualTo("bar");
65+
assertThat(getAndValidateLastLogLine().get(CUSTOM_MDC_SERIALIZER_TEST_KEY).textValue()).isEqualTo("some_text_lower_case");
6366
}
6467

6568
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*-
2+
* #%L
3+
* Java ECS logging
4+
* %%
5+
* Copyright (C) 2019 - 2020 Elastic and contributors
6+
* %%
7+
* Licensed to Elasticsearch B.V. under one or more contributor
8+
* license agreements. See the NOTICE file distributed with
9+
* this work for additional information regarding copyright
10+
* ownership. Elasticsearch B.V. licenses this file to you under
11+
* the Apache License, Version 2.0 (the "License"); you may
12+
* not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing,
18+
* software distributed under the License is distributed on an
19+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20+
* KIND, either express or implied. See the License for the
21+
* specific language governing permissions and limitations
22+
* under the License.
23+
* #L%
24+
*/
25+
package co.elastic.logging.log4j2;
26+
27+
import org.apache.logging.log4j.core.LogEvent;
28+
import org.apache.logging.log4j.util.TriConsumer;
29+
30+
import co.elastic.logging.EcsJsonSerializer;
31+
import co.elastic.logging.JsonUtils;
32+
33+
public class CustomMdcSerializer implements MdcSerializer {
34+
35+
protected static final String CUSTOM_MDC_SERIALIZER_TEST_KEY = "SPECIAL_TEST_CUSTOM_KEY";
36+
37+
@Override
38+
public void serializeMdc(LogEvent event, StringBuilder builder) {
39+
event.getContextData()
40+
.forEach((key, value) -> getWriteFunctionForKey(key).accept(key, value, builder));
41+
}
42+
43+
// Default function for serializing MDC entries
44+
private static final TriConsumer<String, Object, StringBuilder> DEFAULT_WRITE_MDC_FUNCTION = (key, value, stringBuilder) -> {
45+
stringBuilder.append('\"');
46+
JsonUtils.quoteAsString(key, stringBuilder);
47+
stringBuilder.append("\":\"");
48+
JsonUtils.quoteAsString(EcsJsonSerializer.toNullSafeString(String.valueOf(value)), stringBuilder);
49+
stringBuilder.append("\",");
50+
};
51+
52+
// Custom function for handling a specific key
53+
private static final TriConsumer<String, Object, StringBuilder> CUSTOM_KEY_WRITE_MDC_FUNCTION = (key, value, stringBuilder) -> DEFAULT_WRITE_MDC_FUNCTION.accept(
54+
key,
55+
value.toString().toUpperCase(),
56+
stringBuilder
57+
);
58+
59+
/**
60+
* Returns the appropriate function to write an MDC entry based on the key.
61+
*
62+
* @param key MDC key.
63+
* @return The function to serialize the MDC entry value.
64+
*/
65+
private TriConsumer<String, Object, StringBuilder> getWriteFunctionForKey(String key) {
66+
if (CUSTOM_MDC_SERIALIZER_TEST_KEY.equals(key)) {
67+
return CUSTOM_KEY_WRITE_MDC_FUNCTION;
68+
}
69+
return DEFAULT_WRITE_MDC_FUNCTION;
70+
}
71+
}

0 commit comments

Comments
 (0)