Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<key>`, where `<key>` 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
Expand Down
22 changes: 8 additions & 14 deletions sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -160,6 +161,12 @@ protected void captureLog(@NotNull LogRecord loggingEvent) {
attributes.add(SentryAttribute.stringAttribute("sentry.message.template", message));
}

final @Nullable Map<String, String> mdcProperties = MDC.getMDCAdapter().getCopyOfContextMap();
if (mdcProperties != null) {
final List<String> contextTags = ScopesAdapter.getInstance().getOptions().getContextTags();
LoggerPropertiesUtil.applyPropertiesToAttributes(attributes, contextTags, mdcProperties);
}

final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);
params.setOrigin("auto.log.jul");

Expand Down Expand Up @@ -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<String> 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());
Expand Down
22 changes: 22 additions & 0 deletions sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -230,6 +231,11 @@ protected void captureLog(@NotNull LogEvent loggingEvent) {
SentryAttribute.stringAttribute("sentry.message.template", nonFormattedMessage));
}

final @NotNull Map<String, String> contextData = loggingEvent.getContextData().toMap();
final @NotNull List<String> contextTags =
ScopesAdapter.getInstance().getOptions().getContextTags();
LoggerPropertiesUtil.applyPropertiesToAttributes(attributes, contextTags, contextData);

final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);
params.setOrigin("auto.log.log4j2");

Expand Down Expand Up @@ -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<String> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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;
Expand Down Expand Up @@ -195,6 +183,11 @@ protected void captureLog(@NotNull ILoggingEvent loggingEvent) {
arguments = loggingEvent.getArgumentArray();
}

final @NotNull Map<String, String> mdcProperties = loggingEvent.getMDCPropertyMap();
final @NotNull List<String> contextTags =
ScopesAdapter.getInstance().getOptions().getContextTags();
LoggerPropertiesUtil.applyPropertiesToAttributes(attributes, contextTags, mdcProperties);

final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);
params.setOrigin("auto.log.logback");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
}
)
}
}
7 changes: 7 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()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 <init> (Ljava/util/Map;)V
public fun beginArray ()V
Expand Down
76 changes: 76 additions & 0 deletions sentry/src/main/java/io/sentry/util/LoggerPropertiesUtil.java
Original file line number Diff line number Diff line change
@@ -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<String> targetKeys,
final @NotNull Map<String, String> 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<String> targetKeys,
final @NotNull Map<String, String> 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<String> targetKeys,
final @NotNull Map<String, String> 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));
}
}
}
}
}
Loading