Skip to content

Commit 7e441fb

Browse files
authored
Support MultiformatMessage JSON format (#30)
1 parent 6124f79 commit 7e441fb

File tree

3 files changed

+108
-8
lines changed

3 files changed

+108
-8
lines changed

log4j2-ecs-layout/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,46 @@ set the `includeMarkers` attribute to `true` (default: `false`).
4646
</Loggers>
4747
</Configuration>
4848
```
49+
50+
## Structured logging
51+
52+
By leveraging log4j2's `MapMessage` or even by implementing your own `MultiformatMessage` with JSON support,
53+
you can add additional fields to the resulting JSON.
54+
55+
Example:
56+
57+
```java
58+
logger.info(new StringMapMessage().with("message", "foo").with("foo", "bar"));
59+
```
60+
61+
### Gotchas
62+
63+
A common pitfall is how dots in field names are handled in Elasticsearch and how they affect the mapping.
64+
In recent Elasticsearch versions, the following JSON structures would result in the same index mapping:
65+
66+
```json
67+
{
68+
"foo.bar": "baz"
69+
}
70+
```
71+
72+
```json
73+
{
74+
"foo": {
75+
"bar": "baz"
76+
}
77+
}
78+
```
79+
The property `foo` would be mapped to the [Object datatype](https://www.elastic.co/guide/en/elasticsearch/reference/current/object.html).
80+
81+
This means that you can't index a document where `foo` would be a different datatype, as in shown in the following example:
82+
83+
```json
84+
{
85+
"foo": "bar"
86+
}
87+
```
88+
89+
In that example, `foo` is a string.
90+
Trying to index that document results in an error because the data type of `foo` can't be object and string at the same time.
91+

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

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@
4242
import org.apache.logging.log4j.core.lookup.StrSubstitutor;
4343
import org.apache.logging.log4j.core.util.KeyValuePair;
4444
import org.apache.logging.log4j.core.util.StringBuilderWriter;
45-
import org.apache.logging.log4j.message.MapMessage;
4645
import org.apache.logging.log4j.message.Message;
46+
import org.apache.logging.log4j.message.MultiformatMessage;
47+
import org.apache.logging.log4j.util.MultiFormatStringBuilderFormattable;
4748
import org.apache.logging.log4j.util.StringBuilderFormattable;
4849
import org.apache.logging.log4j.util.TriConsumer;
4950

@@ -55,12 +56,15 @@
5556
import java.util.HashSet;
5657
import java.util.List;
5758
import java.util.Set;
59+
import java.util.concurrent.ConcurrentHashMap;
60+
import java.util.concurrent.ConcurrentMap;
5861

5962
@Plugin(name = "EcsLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE)
6063
public class EcsLayout extends AbstractStringLayout {
6164

6265
private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<StringBuilder>();
6366
public static final Charset UTF_8 = Charset.forName("UTF-8");
67+
public static final String[] JSON_FORMAT = {"JSON"};
6468

6569
private final TriConsumer<String, Object, StringBuilder> WRITE_KEY_VALUES_INTO = new TriConsumer<String, Object, StringBuilder>() {
6670
@Override
@@ -80,6 +84,7 @@ public void accept(final String key, final Object value, final StringBuilder str
8084
private final Set<String> topLevelLabels;
8185
private String serviceName;
8286
private boolean includeMarkers;
87+
private final ConcurrentMap<Class<? extends MultiformatMessage>, Boolean> supportsJson = new ConcurrentHashMap<Class<? extends MultiformatMessage>, Boolean>();
8388

8489
private EcsLayout(Configuration config, String serviceName, boolean includeMarkers, KeyValuePair[] additionalFields, Collection<String> topLevelLabels) {
8590
super(config, UTF_8, null, null);
@@ -201,6 +206,37 @@ private void serializeMarker(StringBuilder builder, Marker marker) {
201206
}
202207

203208
private void serializeMessage(StringBuilder builder, boolean gcFree, Message message, Throwable thrown) {
209+
if (message instanceof MultiformatMessage) {
210+
MultiformatMessage multiformatMessage = (MultiformatMessage) message;
211+
if (supportsJson(multiformatMessage)) {
212+
serializeJsonMessage(builder, multiformatMessage);
213+
} else {
214+
serializeSimpleMessage(builder, gcFree, message, thrown);
215+
}
216+
} else {
217+
serializeSimpleMessage(builder, gcFree, message, thrown);
218+
}
219+
}
220+
221+
private void serializeJsonMessage(StringBuilder builder, MultiformatMessage message) {
222+
final StringBuilder messageBuffer = getMessageStringBuilder();
223+
if (message instanceof MultiFormatStringBuilderFormattable) {
224+
((MultiFormatStringBuilderFormattable) message).formatTo(JSON_FORMAT, messageBuffer);
225+
} else {
226+
messageBuffer.append(message.getFormattedMessage(JSON_FORMAT));
227+
}
228+
if (isObject(messageBuffer)) {
229+
moveToRoot(messageBuffer);
230+
builder.append(messageBuffer);
231+
builder.append(", ");
232+
} else {
233+
builder.append("\"message\":");
234+
builder.append(messageBuffer);
235+
builder.append(", ");
236+
}
237+
}
238+
239+
private void serializeSimpleMessage(StringBuilder builder, boolean gcFree, Message message, Throwable thrown) {
204240
builder.append("\"message\":\"");
205241
if (message instanceof CharSequence) {
206242
JsonUtils.quoteAsString(((CharSequence) message), builder);
@@ -220,10 +256,30 @@ private void serializeMessage(StringBuilder builder, boolean gcFree, Message mes
220256
JsonUtils.quoteAsString(formatThrowable(thrown), builder);
221257
}
222258
builder.append("\", ");
223-
if (message instanceof MapMessage) {
224-
MapMessage mapMessage = (MapMessage) message;
225-
mapMessage.forEach(WRITE_KEY_VALUES_INTO, builder);
259+
}
260+
261+
private boolean isObject(StringBuilder messageBuffer) {
262+
return messageBuffer.length() > 1 && messageBuffer.charAt(0) == '{' && messageBuffer.charAt(messageBuffer.length() -1) == '}';
263+
}
264+
265+
private void moveToRoot(StringBuilder messageBuffer) {
266+
messageBuffer.setCharAt(0, ' ');
267+
messageBuffer.setCharAt(messageBuffer.length() -1, ' ');
268+
}
269+
270+
private boolean supportsJson(MultiformatMessage message) {
271+
Boolean supportsJson = this.supportsJson.get(message.getClass());
272+
if (supportsJson == null) {
273+
supportsJson = false;
274+
for (String format : message.getFormats()) {
275+
if (format.equalsIgnoreCase("JSON")) {
276+
supportsJson = true;
277+
break;
278+
}
279+
}
280+
this.supportsJson.put(message.getClass(), supportsJson);
226281
}
282+
return supportsJson;
227283
}
228284

229285
private static CharSequence formatThrowable(final Throwable throwable) {

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package co.elastic.logging.log4j2;
2626

2727
import co.elastic.logging.AbstractEcsLoggingTest;
28+
import com.fasterxml.jackson.databind.JsonNode;
2829
import com.fasterxml.jackson.databind.node.TextNode;
2930
import org.apache.logging.log4j.Marker;
3031
import org.apache.logging.log4j.MarkerManager;
@@ -35,8 +36,6 @@
3536
import org.junit.jupiter.api.AfterEach;
3637
import org.junit.jupiter.api.Test;
3738

38-
import java.util.Map;
39-
4039
import static org.assertj.core.api.Assertions.assertThat;
4140

4241
abstract class AbstractLog4j2EcsLayoutTest extends AbstractEcsLoggingTest {
@@ -72,8 +71,10 @@ void testMarker() throws Exception {
7271

7372
@Test
7473
void testMapMessage() throws Exception {
75-
root.info(new StringMapMessage(Map.of("foo", "bar")));
76-
assertThat(getLastLogLine().get("labels.foo").textValue()).isEqualTo("bar");
74+
root.info(new StringMapMessage().with("message", "foo").with("foo", "bar"));
75+
JsonNode log = getLastLogLine();
76+
assertThat(log.get("message").textValue()).isEqualTo("foo");
77+
assertThat(log.get("foo").textValue()).isEqualTo("bar");
7778
}
7879

7980
@Override

0 commit comments

Comments
 (0)