Skip to content

Commit 9b37b7e

Browse files
authored
Merge 5ca0e5a into cf708bd
2 parents cf708bd + 5ca0e5a commit 9b37b7e

File tree

9 files changed

+172
-48
lines changed

9 files changed

+172
-48
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44

55
### Features
66

7+
- Attach MDC properties to logs as attributes ([#4786](https://github.com/getsentry/sentry-java/pull/4786))
8+
- MDC properties set using supported logging frameworks (Logback, Log4j2, java.util.Logging) are now attached to structured logs as attributes.
9+
- The attribute reflected on the log is `mdc.<key>`, where `<key>` is the original key in the MDC.
10+
- This means that you will be able to filter/aggregate logs in the product based on these properties.
11+
- Only properties with keys matching the configured `contextTags` are sent as log attributes.
12+
- 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`.
13+
- Note that keys containing spaces are not supported.
714
- 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))
815

916
### Fixes

sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.sentry.protocol.Message;
2323
import io.sentry.protocol.SdkVersion;
2424
import io.sentry.util.CollectionUtils;
25+
import io.sentry.util.LoggerPropertiesUtil;
2526
import java.text.MessageFormat;
2627
import java.util.ArrayList;
2728
import java.util.Date;
@@ -160,6 +161,12 @@ protected void captureLog(@NotNull LogRecord loggingEvent) {
160161
attributes.add(SentryAttribute.stringAttribute("sentry.message.template", message));
161162
}
162163

164+
final @Nullable Map<String, String> mdcProperties = MDC.getMDCAdapter().getCopyOfContextMap();
165+
if (mdcProperties != null) {
166+
final List<String> contextTags = ScopesAdapter.getInstance().getOptions().getContextTags();
167+
LoggerPropertiesUtil.applyPropertiesToAttributes(attributes, contextTags, mdcProperties);
168+
}
169+
163170
final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);
164171
params.setOrigin("auto.log.jul");
165172

@@ -312,20 +319,7 @@ SentryEvent createEvent(final @NotNull LogRecord record) {
312319
// get tags from ScopesAdapter options to allow getting the correct tags if Sentry has been
313320
// initialized somewhere else
314321
final List<String> contextTags = ScopesAdapter.getInstance().getOptions().getContextTags();
315-
if (!contextTags.isEmpty()) {
316-
for (final String contextTag : contextTags) {
317-
// if mdc tag is listed in SentryOptions, apply as event tag
318-
if (mdcProperties.containsKey(contextTag)) {
319-
event.setTag(contextTag, mdcProperties.get(contextTag));
320-
// remove from all tags applied to logging event
321-
mdcProperties.remove(contextTag);
322-
}
323-
}
324-
}
325-
// put the rest of mdc tags in contexts
326-
if (!mdcProperties.isEmpty()) {
327-
event.getContexts().put("MDC", mdcProperties);
328-
}
322+
LoggerPropertiesUtil.applyPropertiesToEvent(event, contextTags, mdcProperties);
329323
}
330324
}
331325
event.setExtra(THREAD_ID, record.getThreadID());

sentry-jul/src/test/kotlin/io/sentry/jul/SentryHandlerTest.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,4 +555,26 @@ class SentryHandlerTest {
555555
}
556556
)
557557
}
558+
559+
@Test
560+
fun `sets properties from MDC as attributes on logs`() {
561+
fixture = Fixture(minimumLevel = Level.INFO, contextTags = listOf("someTag"))
562+
563+
MDC.put("someTag", "someValue")
564+
MDC.put("otherTag", "otherValue")
565+
fixture.logger.info("testing MDC properties in logs")
566+
567+
Sentry.flush(1000)
568+
569+
verify(fixture.transport)
570+
.send(
571+
checkLogs { logs ->
572+
val log = logs.items.first()
573+
assertEquals("testing MDC properties in logs", log.body)
574+
val attributes = log.attributes!!
575+
assertEquals("someValue", attributes["mdc.someTag"]?.value)
576+
assertNull(attributes["otherTag"])
577+
}
578+
)
579+
}
558580
}

sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.sentry.protocol.Message;
2626
import io.sentry.protocol.SdkVersion;
2727
import io.sentry.util.CollectionUtils;
28+
import io.sentry.util.LoggerPropertiesUtil;
2829
import java.util.Arrays;
2930
import java.util.Collections;
3031
import java.util.List;
@@ -230,6 +231,11 @@ protected void captureLog(@NotNull LogEvent loggingEvent) {
230231
SentryAttribute.stringAttribute("sentry.message.template", nonFormattedMessage));
231232
}
232233

234+
final @NotNull Map<String, String> contextData = loggingEvent.getContextData().toMap();
235+
final @NotNull List<String> contextTags =
236+
ScopesAdapter.getInstance().getOptions().getContextTags();
237+
LoggerPropertiesUtil.applyPropertiesToAttributes(attributes, contextTags, contextData);
238+
233239
final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);
234240
params.setOrigin("auto.log.log4j2");
235241

@@ -279,20 +285,7 @@ protected void captureLog(@NotNull LogEvent loggingEvent) {
279285
// get tags from ScopesAdapter options to allow getting the correct tags if Sentry has been
280286
// initialized somewhere else
281287
final List<String> contextTags = scopes.getOptions().getContextTags();
282-
if (contextTags != null && !contextTags.isEmpty()) {
283-
for (final String contextTag : contextTags) {
284-
// if mdc tag is listed in SentryOptions, apply as event tag
285-
if (contextData.containsKey(contextTag)) {
286-
event.setTag(contextTag, contextData.get(contextTag));
287-
// remove from all tags applied to logging event
288-
contextData.remove(contextTag);
289-
}
290-
}
291-
}
292-
// put the rest of mdc tags in contexts
293-
if (!contextData.isEmpty()) {
294-
event.getContexts().put("Context Data", contextData);
295-
}
288+
LoggerPropertiesUtil.applyPropertiesToEvent(event, contextTags, contextData);
296289
}
297290

298291
return event;

sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,7 @@ class SentryAppenderTest {
334334

335335
verify(fixture.transport)
336336
.send(
337-
checkEvent { event ->
338-
assertEquals(mapOf("key" to "value"), event.contexts["Context Data"])
339-
},
337+
checkEvent { event -> assertEquals(mapOf("key" to "value"), event.contexts["MDC"]) },
340338
anyOrNull(),
341339
)
342340
}
@@ -351,7 +349,7 @@ class SentryAppenderTest {
351349
verify(fixture.transport)
352350
.send(
353351
checkEvent { event ->
354-
assertEquals(mapOf("key" to "value"), event.contexts["Context Data"])
352+
assertEquals(mapOf("key" to "value"), event.contexts["MDC"])
355353
assertEquals(mapOf("contextTag1" to "contextTag1Value"), event.tags)
356354
},
357355
anyOrNull(),
@@ -368,7 +366,7 @@ class SentryAppenderTest {
368366
verify(fixture.transport)
369367
.send(
370368
checkEvent { event ->
371-
assertNotNull(event.contexts["Context Data"]) {
369+
assertNotNull(event.contexts["MDC"]) {
372370
val contextData = it as Map<*, *>
373371
assertNull(contextData["key1"])
374372
assertEquals("value", contextData["key2"])
@@ -386,7 +384,7 @@ class SentryAppenderTest {
386384
logger.warn("testing MDC tags")
387385

388386
verify(fixture.transport)
389-
.send(checkEvent { event -> assertNull(event.contexts["Context Data"]) }, anyOrNull())
387+
.send(checkEvent { event -> assertNull(event.contexts["MDC"]) }, anyOrNull())
390388
}
391389

392390
@Test
@@ -591,4 +589,27 @@ class SentryAppenderTest {
591589
}
592590
)
593591
}
592+
593+
@Test
594+
fun `sets properties from ThreadContext as attributes on logs`() {
595+
val logger = fixture.getSut(minimumLevel = Level.INFO, contextTags = listOf("someTag"))
596+
ScopesAdapter.getInstance().options.logs.isEnabled = true
597+
598+
ThreadContext.put("someTag", "someValue")
599+
ThreadContext.put("otherTag", "otherValue")
600+
logger.info("testing MDC properties in logs")
601+
602+
Sentry.flush(1000)
603+
604+
verify(fixture.transport)
605+
.send(
606+
checkLogs { logs ->
607+
val log = logs.items.first()
608+
assertEquals("testing MDC properties in logs", log.body)
609+
val attributes = log.attributes!!
610+
assertEquals("someValue", attributes["mdc.someTag"]?.value)
611+
assertNull(attributes["otherTag"])
612+
}
613+
)
614+
}
594615
}

sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import io.sentry.protocol.Message;
3030
import io.sentry.protocol.SdkVersion;
3131
import io.sentry.util.CollectionUtils;
32+
import io.sentry.util.LoggerPropertiesUtil;
3233
import java.nio.charset.StandardCharsets;
3334
import java.util.Arrays;
3435
import java.util.Collections;
@@ -152,20 +153,7 @@ protected void append(@NotNull ILoggingEvent eventObject) {
152153
// get tags from ScopesAdapter options to allow getting the correct tags if Sentry has been
153154
// initialized somewhere else
154155
final List<String> contextTags = ScopesAdapter.getInstance().getOptions().getContextTags();
155-
if (!contextTags.isEmpty()) {
156-
for (final String contextTag : contextTags) {
157-
// if mdc tag is listed in SentryOptions, apply as event tag
158-
if (mdcProperties.containsKey(contextTag)) {
159-
event.setTag(contextTag, mdcProperties.get(contextTag));
160-
// remove from all tags applied to logging event
161-
mdcProperties.remove(contextTag);
162-
}
163-
}
164-
}
165-
// put the rest of mdc tags in contexts
166-
if (!mdcProperties.isEmpty()) {
167-
event.getContexts().put("MDC", mdcProperties);
168-
}
156+
LoggerPropertiesUtil.applyPropertiesToEvent(event, contextTags, mdcProperties);
169157
}
170158

171159
return event;
@@ -195,6 +183,11 @@ protected void captureLog(@NotNull ILoggingEvent loggingEvent) {
195183
arguments = loggingEvent.getArgumentArray();
196184
}
197185

186+
final @NotNull Map<String, String> mdcProperties = loggingEvent.getMDCPropertyMap();
187+
final @NotNull List<String> contextTags =
188+
ScopesAdapter.getInstance().getOptions().getContextTags();
189+
LoggerPropertiesUtil.applyPropertiesToAttributes(attributes, contextTags, mdcProperties);
190+
198191
final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);
199192
params.setOrigin("auto.log.logback");
200193

sentry-logback/src/test/kotlin/io/sentry/logback/SentryAppenderTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,4 +821,25 @@ class SentryAppenderTest {
821821
}
822822
)
823823
}
824+
825+
@Test
826+
fun `sets properties from MDC as attributes on logs`() {
827+
fixture = Fixture(minimumLevel = Level.INFO, enableLogs = true, contextTags = listOf("someTag"))
828+
MDC.put("someTag", "someValue")
829+
MDC.put("otherTag", "otherValue")
830+
fixture.logger.info("testing MDC properties in logs")
831+
832+
Sentry.flush(1000)
833+
834+
verify(fixture.transport)
835+
.send(
836+
checkLogs { logs ->
837+
val log = logs.items.first()
838+
assertEquals("testing MDC properties in logs", log.body)
839+
val attributes = log.attributes!!
840+
assertEquals("someValue", attributes["mdc.someTag"]?.value)
841+
assertNull(attributes["otherTag"])
842+
}
843+
)
844+
}
824845
}

sentry/api/sentry.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7087,6 +7087,12 @@ public final class io/sentry/util/LogUtils {
70877087
public static fun logNotInstanceOf (Ljava/lang/Class;Ljava/lang/Object;Lio/sentry/ILogger;)V
70887088
}
70897089

7090+
public final class io/sentry/util/LoggerPropertiesUtil {
7091+
public fun <init> ()V
7092+
public static fun applyPropertiesToAttributes (Lio/sentry/SentryAttributes;Ljava/util/List;Ljava/util/Map;)V
7093+
public static fun applyPropertiesToEvent (Lio/sentry/SentryEvent;Ljava/util/List;Ljava/util/Map;)V
7094+
}
7095+
70907096
public final class io/sentry/util/MapObjectReader : io/sentry/ObjectReader {
70917097
public fun <init> (Ljava/util/Map;)V
70927098
public fun beginArray ()V
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.sentry.util;
2+
3+
import io.sentry.SentryAttribute;
4+
import io.sentry.SentryAttributes;
5+
import io.sentry.SentryEvent;
6+
import java.util.List;
7+
import java.util.Map;
8+
import org.jetbrains.annotations.ApiStatus;
9+
import org.jetbrains.annotations.NotNull;
10+
import org.jetbrains.annotations.Nullable;
11+
12+
/** Utility class for applying logger properties (e.g. MDC) to Sentry events and log attributes. */
13+
@ApiStatus.Internal
14+
public final class LoggerPropertiesUtil {
15+
16+
/**
17+
* Applies logger properties from a map to a Sentry event as tags and context. The properties that
18+
* have keys matching any of the `targetKeys` will be applied as tags, while the others will be
19+
* reported in the `MDC` context.
20+
*
21+
* @param event the Sentry event to add tags to
22+
* @param targetKeys the list of property keys to apply as tags
23+
* @param properties the properties map (e.g. MDC) - this map will be modified by removing
24+
* properties which were applied as tags
25+
*/
26+
@ApiStatus.Internal
27+
public static void applyPropertiesToEvent(
28+
final @NotNull SentryEvent event,
29+
final @NotNull List<String> targetKeys,
30+
final @NotNull Map<String, String> properties) {
31+
if (!targetKeys.isEmpty() && !properties.isEmpty()) {
32+
for (final String key : targetKeys) {
33+
final @Nullable String value = properties.remove(key);
34+
if (value != null) {
35+
event.setTag(key, value);
36+
}
37+
}
38+
}
39+
if (!properties.isEmpty()) {
40+
event.getContexts().put("MDC", properties);
41+
}
42+
}
43+
44+
/**
45+
* Applies logger properties from a properties map to SentryAttributes for logs. Only the
46+
* properties with keys that are found in `targetKeys` will be applied as attributes. Properties
47+
* with null values are filtered out.
48+
*
49+
* @param attributes the SentryAttributes to add the properties to
50+
* @param targetKeys the list of property keys to apply as attributes
51+
* @param properties the properties map (e.g. MDC)
52+
*/
53+
@ApiStatus.Internal
54+
public static void applyPropertiesToAttributes(
55+
final @NotNull SentryAttributes attributes,
56+
final @NotNull List<String> targetKeys,
57+
final @NotNull Map<String, String> properties) {
58+
if (!targetKeys.isEmpty() && !properties.isEmpty()) {
59+
for (final String key : targetKeys) {
60+
final @Nullable String value = properties.get(key);
61+
if (value != null) {
62+
attributes.add(SentryAttribute.stringAttribute("mdc." + key, value));
63+
}
64+
}
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)