diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java index bac717700d0e..324ac47911f1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -17,8 +17,11 @@ package org.springframework.boot.logging.log4j2; import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.impl.ThrowableProxy; import org.apache.logging.log4j.core.time.Instant; @@ -66,6 +69,10 @@ private static void jsonMembers(Environment environment, JsonWriter.Members getMarkers(Marker marker) { + Set result = new TreeSet<>(); + addMarkers(result, marker); + return result; + } + + private static void addMarkers(Set result, Marker marker) { + result.add(marker.getName()); + if (marker.hasParents()) { + for (Marker parent : marker.getParents()) { + addMarkers(result, parent); + } + } + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java index ef8511d831b7..4f80adbff554 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -16,11 +16,16 @@ package org.springframework.boot.logging.logback; +import java.util.Iterator; +import java.util.List; import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; import ch.qos.logback.classic.pattern.ThrowableProxyConverter; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.IThrowableProxy; +import org.slf4j.Marker; import org.slf4j.event.KeyValuePair; import org.springframework.boot.json.JsonWriter; @@ -69,6 +74,26 @@ private static void jsonMembers(Environment environment, ThrowableProxyConverter throwableMembers.add("error.stack_trace", throwableProxyConverter::convert); }); members.add("ecs.version", "8.11"); + members.add("tags", ILoggingEvent::getMarkerList) + .whenNotNull() + .as(ElasticCommonSchemaStructuredLogFormatter::getMarkers) + .whenNotEmpty(); + } + + private static Set getMarkers(List markers) { + Set result = new TreeSet<>(); + addMarkers(result, markers.iterator()); + return result; + } + + private static void addMarkers(Set result, Iterator iterator) { + while (iterator.hasNext()) { + Marker marker = iterator.next(); + result.add(marker.getName()); + if (marker.hasReferences()) { + addMarkers(result, marker.iterator()); + } + } } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java index 0ffb0a88e1e1..3e9eb3cd0819 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/ElasticCommonSchemaStructuredLogFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -16,8 +16,11 @@ package org.springframework.boot.logging.log4j2; +import java.util.List; import java.util.Map; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap; import org.apache.logging.log4j.core.impl.MutableLogEvent; import org.apache.logging.log4j.message.MapMessage; @@ -100,4 +103,29 @@ void shouldFormatStructuredMessage() { "org.example.Test", "message", expectedMessage, "ecs.version", "8.11")); } + @Test + void shouldFormatMarkersAsTags() { + MutableLogEvent event = createEvent(); + + Marker parent = MarkerManager.getMarker("parent"); + parent.addParents(MarkerManager.getMarker("grandparent")); + + Marker parent1 = MarkerManager.getMarker("parent1"); + parent1.addParents(MarkerManager.getMarker("grandparent1")); + + Marker grandchild = MarkerManager.getMarker("grandchild"); + grandchild.addParents(parent); + grandchild.addParents(parent1); + event.setMarker(grandchild); + + String json = this.formatter.format(event); + assertThat(json).endsWith("\n"); + Map deserialized = deserialize(json); + assertThat(deserialized).containsExactlyInAnyOrderEntriesOf(map("@timestamp", "2024-07-02T08:49:53Z", + "log.level", "INFO", "process.pid", 1, "process.thread.name", "main", "service.name", "name", + "service.version", "1.0.0", "service.environment", "test", "service.node.name", "node-1", "log.logger", + "org.example.Test", "message", "message", "ecs.version", "8.11", "tags", + List.of("grandchild", "grandparent", "grandparent1", "parent", "parent1"))); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java index 6c2c355cccbb..5ccb73e5ac12 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/ElasticCommonSchemaStructuredLogFormatterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -17,12 +17,15 @@ package org.springframework.boot.logging.logback; import java.util.Collections; +import java.util.List; import java.util.Map; import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.classic.spi.ThrowableProxy; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; import org.springframework.mock.env.MockEnvironment; @@ -92,4 +95,30 @@ void shouldFormatException() { .replace("\r", "\\r")); } + @Test + void shouldFormatMarkersAsTags() { + LoggingEvent event = createEvent(); + event.setMDCPropertyMap(Collections.emptyMap()); + + Marker parent = MarkerFactory.getDetachedMarker("parent"); + parent.add(MarkerFactory.getDetachedMarker("child")); + + Marker parent1 = MarkerFactory.getDetachedMarker("parent1"); + parent1.add(MarkerFactory.getDetachedMarker("child1")); + + Marker grandparent = MarkerFactory.getMarker("grandparent"); + grandparent.add(parent); + grandparent.add(parent1); + event.addMarker(grandparent); + + String json = this.formatter.format(event); + assertThat(json).endsWith("\n"); + Map deserialized = deserialize(json); + assertThat(deserialized).containsExactlyInAnyOrderEntriesOf( + map("@timestamp", "2024-07-02T08:49:53Z", "log.level", "INFO", "process.pid", 1, "process.thread.name", + "main", "service.name", "name", "service.version", "1.0.0", "service.environment", "test", + "service.node.name", "node-1", "log.logger", "org.example.Test", "message", "message", + "ecs.version", "8.11", "tags", List.of("child", "child1", "grandparent", "parent", "parent1"))); + } + }