Skip to content

Commit ab68dda

Browse files
committed
Add marker information to ECS structured logging
See gh-43728 Signed-off-by: Dmytro Nosan <[email protected]>
1 parent 83a5338 commit ab68dda

File tree

4 files changed

+123
-4
lines changed

4 files changed

+123
-4
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,8 +17,11 @@
1717
package org.springframework.boot.logging.log4j2;
1818

1919
import java.util.Objects;
20+
import java.util.Set;
21+
import java.util.TreeSet;
2022

2123
import org.apache.logging.log4j.Level;
24+
import org.apache.logging.log4j.Marker;
2225
import org.apache.logging.log4j.core.LogEvent;
2326
import org.apache.logging.log4j.core.impl.ThrowableProxy;
2427
import org.apache.logging.log4j.core.time.Instant;
@@ -32,6 +35,7 @@
3235
import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer;
3336
import org.springframework.core.env.Environment;
3437
import org.springframework.util.ObjectUtils;
38+
import org.springframework.util.StringUtils;
3539

3640
/**
3741
* Log4j2 {@link StructuredLogFormatter} for
@@ -66,11 +70,34 @@ private static void jsonMembers(Environment environment, JsonWriter.Members<LogE
6670
thrownProxyMembers.add("error.message", ThrowableProxy::getMessage);
6771
thrownProxyMembers.add("error.stack_trace", ThrowableProxy::getExtendedStackTraceAsString);
6872
});
73+
members.add("tags", ElasticCommonSchemaStructuredLogFormatter::asTags).whenNotEmpty();
6974
members.add("ecs.version", "8.11");
7075
}
7176

7277
private static java.time.Instant asTimestamp(Instant instant) {
7378
return java.time.Instant.ofEpochMilli(instant.getEpochMillisecond()).plusNanos(instant.getNanoOfMillisecond());
7479
}
7580

81+
private static Set<String> asTags(LogEvent event) {
82+
Set<String> tags = new TreeSet<>();
83+
collectTags(event.getMarker(), tags);
84+
return tags;
85+
}
86+
87+
private static void collectTags(Marker marker, Set<String> tags) {
88+
if (marker == null) {
89+
return;
90+
}
91+
if (StringUtils.hasText(marker.getName())) {
92+
tags.add(marker.getName());
93+
}
94+
Marker[] parents = marker.getParents();
95+
if (parents == null) {
96+
return;
97+
}
98+
for (Marker parent : parents) {
99+
collectTags(parent, tags);
100+
}
101+
}
102+
76103
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,11 +16,16 @@
1616

1717
package org.springframework.boot.logging.logback;
1818

19+
import java.util.Iterator;
20+
import java.util.List;
1921
import java.util.Objects;
22+
import java.util.Set;
23+
import java.util.TreeSet;
2024

2125
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
2226
import ch.qos.logback.classic.spi.ILoggingEvent;
2327
import ch.qos.logback.classic.spi.IThrowableProxy;
28+
import org.slf4j.Marker;
2429
import org.slf4j.event.KeyValuePair;
2530

2631
import org.springframework.boot.json.JsonWriter;
@@ -31,6 +36,7 @@
3136
import org.springframework.boot.logging.structured.StructuredLogFormatter;
3237
import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer;
3338
import org.springframework.core.env.Environment;
39+
import org.springframework.util.StringUtils;
3440

3541
/**
3642
* Logback {@link StructuredLogFormatter} for
@@ -69,6 +75,35 @@ private static void jsonMembers(Environment environment, ThrowableProxyConverter
6975
throwableMembers.add("error.stack_trace", throwableProxyConverter::convert);
7076
});
7177
members.add("ecs.version", "8.11");
78+
members.add("tags", ElasticCommonSchemaStructuredLogFormatter::asTags).whenNotEmpty();
79+
}
80+
81+
private static Set<String> asTags(ILoggingEvent event) {
82+
Set<String> tags = new TreeSet<>();
83+
List<Marker> markers = event.getMarkerList();
84+
if (markers != null) {
85+
collectTags(markers.iterator(), tags);
86+
}
87+
return tags;
88+
}
89+
90+
private static void collectTags(Iterator<Marker> markers, Set<String> tags) {
91+
if (markers == null) {
92+
return;
93+
}
94+
while (markers.hasNext()) {
95+
collectTags(markers.next(), tags);
96+
}
97+
}
98+
99+
private static void collectTags(Marker marker, Set<String> tags) {
100+
if (marker == null) {
101+
return;
102+
}
103+
if (StringUtils.hasText(marker.getName())) {
104+
tags.add(marker.getName());
105+
}
106+
collectTags(marker.iterator(), tags);
72107
}
73108

74109
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,11 @@
1616

1717
package org.springframework.boot.logging.log4j2;
1818

19+
import java.util.List;
1920
import java.util.Map;
2021

22+
import org.apache.logging.log4j.Marker;
23+
import org.apache.logging.log4j.MarkerManager;
2124
import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap;
2225
import org.apache.logging.log4j.core.impl.MutableLogEvent;
2326
import org.apache.logging.log4j.message.MapMessage;
@@ -100,4 +103,29 @@ void shouldFormatStructuredMessage() {
100103
"org.example.Test", "message", expectedMessage, "ecs.version", "8.11"));
101104
}
102105

106+
@Test
107+
void shouldFormatMarkersAsTags() {
108+
MutableLogEvent event = createEvent();
109+
110+
Marker parent = MarkerManager.getMarker("parent");
111+
parent.addParents(MarkerManager.getMarker("grandparent"));
112+
113+
Marker parent1 = MarkerManager.getMarker("parent1");
114+
parent1.addParents(MarkerManager.getMarker("grandparent1"));
115+
116+
Marker grandchild = MarkerManager.getMarker("grandchild");
117+
grandchild.addParents(parent);
118+
grandchild.addParents(parent1);
119+
event.setMarker(grandchild);
120+
121+
String json = this.formatter.format(event);
122+
assertThat(json).endsWith("\n");
123+
Map<String, Object> deserialized = deserialize(json);
124+
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("@timestamp", "2024-07-02T08:49:53Z",
125+
"log.level", "INFO", "process.pid", 1, "process.thread.name", "main", "service.name", "name",
126+
"service.version", "1.0.0", "service.environment", "test", "service.node.name", "node-1", "log.logger",
127+
"org.example.Test", "message", "message", "ecs.version", "8.11", "tags",
128+
List.of("grandchild", "grandparent", "grandparent1", "parent", "parent1")));
129+
}
130+
103131
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2024 the original author or authors.
2+
* Copyright 2012-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,12 +17,15 @@
1717
package org.springframework.boot.logging.logback;
1818

1919
import java.util.Collections;
20+
import java.util.List;
2021
import java.util.Map;
2122

2223
import ch.qos.logback.classic.spi.LoggingEvent;
2324
import ch.qos.logback.classic.spi.ThrowableProxy;
2425
import org.junit.jupiter.api.BeforeEach;
2526
import org.junit.jupiter.api.Test;
27+
import org.slf4j.Marker;
28+
import org.slf4j.MarkerFactory;
2629

2730
import org.springframework.mock.env.MockEnvironment;
2831

@@ -92,4 +95,30 @@ void shouldFormatException() {
9295
.replace("\r", "\\r"));
9396
}
9497

98+
@Test
99+
void shouldFormatMarkersAsTags() {
100+
LoggingEvent event = createEvent();
101+
event.setMDCPropertyMap(Collections.emptyMap());
102+
103+
Marker parent = MarkerFactory.getDetachedMarker("parent");
104+
parent.add(MarkerFactory.getDetachedMarker("child"));
105+
106+
Marker parent1 = MarkerFactory.getDetachedMarker("parent1");
107+
parent1.add(MarkerFactory.getDetachedMarker("child1"));
108+
109+
Marker grandparent = MarkerFactory.getMarker("grandparent");
110+
grandparent.add(parent);
111+
grandparent.add(parent1);
112+
event.addMarker(grandparent);
113+
114+
String json = this.formatter.format(event);
115+
assertThat(json).endsWith("\n");
116+
Map<String, Object> deserialized = deserialize(json);
117+
assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(
118+
map("@timestamp", "2024-07-02T08:49:53Z", "log.level", "INFO", "process.pid", 1, "process.thread.name",
119+
"main", "service.name", "name", "service.version", "1.0.0", "service.environment", "test",
120+
"service.node.name", "node-1", "log.logger", "org.example.Test", "message", "message",
121+
"ecs.version", "8.11", "tags", List.of("child", "child1", "grandparent", "parent", "parent1")));
122+
}
123+
95124
}

0 commit comments

Comments
 (0)