diff --git a/CHANGELOG.md b/CHANGELOG.md index 570cd9cc00..5bef8bbd6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ ### Features +- Attach MDC properties to logs as attributes ([#4786](https://github.com/getsentry/sentry-java/pull/4786)) + - MDC properties set using supported logging frameworks (Logback, Log4j2, java.util.Logging) are now attached to structured logs as attributes. + - The attribute reflected on the log is `mdc.`, where `` is the original key in the MDC. + - This means that you will be able to filter/aggregate logs in the product based on these properties. + - Only properties with keys matching the configured `contextTags` are sent as log attributes. + - You can configure which properties are sent using `options.setContextTags` if initalizing manually, or by specifying a comma-separated list of keys with a `context-tags` entry in `sentry.properties` or `sentry.contex-tags` in `application.properties`. + - Note that keys containing spaces are not supported. - Add experimental Sentry Android Distribution module for integrating with Sentry Build Distribution to check for and install updates ([#4804](https://github.com/getsentry/sentry-java/pull/4804)) ### Fixes diff --git a/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java b/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java index 812fdf184d..4442052dd4 100644 --- a/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java +++ b/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java @@ -22,6 +22,7 @@ import io.sentry.protocol.Message; import io.sentry.protocol.SdkVersion; import io.sentry.util.CollectionUtils; +import io.sentry.util.LoggerPropertiesUtil; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Date; @@ -160,6 +161,12 @@ protected void captureLog(@NotNull LogRecord loggingEvent) { attributes.add(SentryAttribute.stringAttribute("sentry.message.template", message)); } + final @Nullable Map mdcProperties = MDC.getMDCAdapter().getCopyOfContextMap(); + if (mdcProperties != null) { + final List contextTags = ScopesAdapter.getInstance().getOptions().getContextTags(); + LoggerPropertiesUtil.applyPropertiesToAttributes(attributes, contextTags, mdcProperties); + } + final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes); params.setOrigin("auto.log.jul"); @@ -312,20 +319,7 @@ SentryEvent createEvent(final @NotNull LogRecord record) { // get tags from ScopesAdapter options to allow getting the correct tags if Sentry has been // initialized somewhere else final List contextTags = ScopesAdapter.getInstance().getOptions().getContextTags(); - if (!contextTags.isEmpty()) { - for (final String contextTag : contextTags) { - // if mdc tag is listed in SentryOptions, apply as event tag - if (mdcProperties.containsKey(contextTag)) { - event.setTag(contextTag, mdcProperties.get(contextTag)); - // remove from all tags applied to logging event - mdcProperties.remove(contextTag); - } - } - } - // put the rest of mdc tags in contexts - if (!mdcProperties.isEmpty()) { - event.getContexts().put("MDC", mdcProperties); - } + LoggerPropertiesUtil.applyPropertiesToEvent(event, contextTags, mdcProperties); } } event.setExtra(THREAD_ID, record.getThreadID()); diff --git a/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt b/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt index a002c98640..889eb267d1 100644 --- a/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt +++ b/sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt @@ -555,4 +555,26 @@ class SentryHandlerTest { } ) } + + @Test + fun `sets properties from MDC as attributes on logs`() { + fixture = Fixture(minimumLevel = Level.INFO, contextTags = listOf("someTag")) + + MDC.put("someTag", "someValue") + MDC.put("otherTag", "otherValue") + fixture.logger.info("testing MDC properties in logs") + + Sentry.flush(1000) + + verify(fixture.transport) + .send( + checkLogs { logs -> + val log = logs.items.first() + assertEquals("testing MDC properties in logs", log.body) + val attributes = log.attributes!! + assertEquals("someValue", attributes["mdc.someTag"]?.value) + assertNull(attributes["otherTag"]) + } + ) + } } diff --git a/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java b/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java index 7c35febd16..70a0ae48e2 100644 --- a/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java +++ b/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java @@ -25,6 +25,7 @@ import io.sentry.protocol.Message; import io.sentry.protocol.SdkVersion; import io.sentry.util.CollectionUtils; +import io.sentry.util.LoggerPropertiesUtil; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -230,6 +231,11 @@ protected void captureLog(@NotNull LogEvent loggingEvent) { SentryAttribute.stringAttribute("sentry.message.template", nonFormattedMessage)); } + final @NotNull Map contextData = loggingEvent.getContextData().toMap(); + final @NotNull List contextTags = + ScopesAdapter.getInstance().getOptions().getContextTags(); + LoggerPropertiesUtil.applyPropertiesToAttributes(attributes, contextTags, contextData); + final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes); params.setOrigin("auto.log.log4j2"); @@ -279,20 +285,7 @@ protected void captureLog(@NotNull LogEvent loggingEvent) { // get tags from ScopesAdapter options to allow getting the correct tags if Sentry has been // initialized somewhere else final List contextTags = scopes.getOptions().getContextTags(); - if (contextTags != null && !contextTags.isEmpty()) { - for (final String contextTag : contextTags) { - // if mdc tag is listed in SentryOptions, apply as event tag - if (contextData.containsKey(contextTag)) { - event.setTag(contextTag, contextData.get(contextTag)); - // remove from all tags applied to logging event - contextData.remove(contextTag); - } - } - } - // put the rest of mdc tags in contexts - if (!contextData.isEmpty()) { - event.getContexts().put("Context Data", contextData); - } + LoggerPropertiesUtil.applyPropertiesToEvent(event, contextTags, contextData, "Context Data"); } return event; diff --git a/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt b/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt index 185972df90..4f67b8af36 100644 --- a/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt +++ b/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt @@ -591,4 +591,26 @@ class SentryAppenderTest { } ) } + + @Test + fun `sets properties from ThreadContext as attributes on logs`() { + val logger = fixture.getSut(minimumLevel = Level.INFO, contextTags = listOf("someTag")) + + ThreadContext.put("someTag", "someValue") + ThreadContext.put("otherTag", "otherValue") + logger.info("testing MDC properties in logs") + + Sentry.flush(1000) + + verify(fixture.transport) + .send( + checkLogs { logs -> + val log = logs.items.first() + assertEquals("testing MDC properties in logs", log.body) + val attributes = log.attributes!! + assertEquals("someValue", attributes["mdc.someTag"]?.value) + assertNull(attributes["otherTag"]) + } + ) + } } diff --git a/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java b/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java index 65a849d72b..7e8edaca89 100644 --- a/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java +++ b/sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java @@ -29,6 +29,7 @@ import io.sentry.protocol.Message; import io.sentry.protocol.SdkVersion; import io.sentry.util.CollectionUtils; +import io.sentry.util.LoggerPropertiesUtil; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -152,20 +153,7 @@ protected void append(@NotNull ILoggingEvent eventObject) { // get tags from ScopesAdapter options to allow getting the correct tags if Sentry has been // initialized somewhere else final List contextTags = ScopesAdapter.getInstance().getOptions().getContextTags(); - if (!contextTags.isEmpty()) { - for (final String contextTag : contextTags) { - // if mdc tag is listed in SentryOptions, apply as event tag - if (mdcProperties.containsKey(contextTag)) { - event.setTag(contextTag, mdcProperties.get(contextTag)); - // remove from all tags applied to logging event - mdcProperties.remove(contextTag); - } - } - } - // put the rest of mdc tags in contexts - if (!mdcProperties.isEmpty()) { - event.getContexts().put("MDC", mdcProperties); - } + LoggerPropertiesUtil.applyPropertiesToEvent(event, contextTags, mdcProperties); } return event; @@ -195,6 +183,11 @@ protected void captureLog(@NotNull ILoggingEvent loggingEvent) { arguments = loggingEvent.getArgumentArray(); } + final @NotNull Map mdcProperties = loggingEvent.getMDCPropertyMap(); + final @NotNull List contextTags = + ScopesAdapter.getInstance().getOptions().getContextTags(); + LoggerPropertiesUtil.applyPropertiesToAttributes(attributes, contextTags, mdcProperties); + final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes); params.setOrigin("auto.log.logback"); diff --git a/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt b/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt index 8c7acbc572..b2e44ac80b 100644 --- a/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt +++ b/sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt @@ -821,4 +821,25 @@ class SentryAppenderTest { } ) } + + @Test + fun `sets properties from MDC as attributes on logs`() { + fixture = Fixture(minimumLevel = Level.INFO, enableLogs = true, contextTags = listOf("someTag")) + MDC.put("someTag", "someValue") + MDC.put("otherTag", "otherValue") + fixture.logger.info("testing MDC properties in logs") + + Sentry.flush(1000) + + verify(fixture.transport) + .send( + checkLogs { logs -> + val log = logs.items.first() + assertEquals("testing MDC properties in logs", log.body) + val attributes = log.attributes!! + assertEquals("someValue", attributes["mdc.someTag"]?.value) + assertNull(attributes["otherTag"]) + } + ) + } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3964619e0b..ad213521e0 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -7087,6 +7087,13 @@ public final class io/sentry/util/LogUtils { public static fun logNotInstanceOf (Ljava/lang/Class;Ljava/lang/Object;Lio/sentry/ILogger;)V } +public final class io/sentry/util/LoggerPropertiesUtil { + public fun ()V + public static fun applyPropertiesToAttributes (Lio/sentry/SentryAttributes;Ljava/util/List;Ljava/util/Map;)V + public static fun applyPropertiesToEvent (Lio/sentry/SentryEvent;Ljava/util/List;Ljava/util/Map;)V + public static fun applyPropertiesToEvent (Lio/sentry/SentryEvent;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;)V +} + public final class io/sentry/util/MapObjectReader : io/sentry/ObjectReader { public fun (Ljava/util/Map;)V public fun beginArray ()V diff --git a/sentry/src/main/java/io/sentry/util/LoggerPropertiesUtil.java b/sentry/src/main/java/io/sentry/util/LoggerPropertiesUtil.java new file mode 100644 index 0000000000..47e610243f --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/LoggerPropertiesUtil.java @@ -0,0 +1,76 @@ +package io.sentry.util; + +import io.sentry.SentryAttribute; +import io.sentry.SentryAttributes; +import io.sentry.SentryEvent; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Utility class for applying logger properties (e.g. MDC) to Sentry events and log attributes. */ +@ApiStatus.Internal +public final class LoggerPropertiesUtil { + + /** + * Applies logger properties from a map to a Sentry event as tags and context. The properties that + * have keys matching any of the `targetKeys` will be applied as tags, while the others will be + * reported in an ad-hoc context. + * + * @param event the Sentry event to add tags to + * @param targetKeys the list of property keys to apply as tags + * @param properties the properties map (e.g. MDC) - this map will be modified by removing + * properties which were applied as tags + * @param contextName the name of the context to use for leftover properties + */ + @ApiStatus.Internal + public static void applyPropertiesToEvent( + final @NotNull SentryEvent event, + final @NotNull List targetKeys, + final @NotNull Map properties, + final @NotNull String contextName) { + if (!targetKeys.isEmpty() && !properties.isEmpty()) { + for (final String key : targetKeys) { + final @Nullable String value = properties.remove(key); + if (value != null) { + event.setTag(key, value); + } + } + } + if (!properties.isEmpty()) { + event.getContexts().put(contextName, properties); + } + } + + public static void applyPropertiesToEvent( + final @NotNull SentryEvent event, + final @NotNull List targetKeys, + final @NotNull Map properties) { + applyPropertiesToEvent(event, targetKeys, properties, "MDC"); + } + + /** + * Applies logger properties from a properties map to SentryAttributes for logs. Only the + * properties with keys that are found in `targetKeys` will be applied as attributes. Properties + * with null values are filtered out. + * + * @param attributes the SentryAttributes to add the properties to + * @param targetKeys the list of property keys to apply as attributes + * @param properties the properties map (e.g. MDC) + */ + @ApiStatus.Internal + public static void applyPropertiesToAttributes( + final @NotNull SentryAttributes attributes, + final @NotNull List targetKeys, + final @NotNull Map properties) { + if (!targetKeys.isEmpty() && !properties.isEmpty()) { + for (final String key : targetKeys) { + final @Nullable String value = properties.get(key); + if (value != null) { + attributes.add(SentryAttribute.stringAttribute("mdc." + key, value)); + } + } + } + } +}