Skip to content

Commit 8fe41f8

Browse files
committed
Attach MDC properties to logs as attributes
1 parent 14ff5ee commit 8fe41f8

File tree

9 files changed

+257
-110
lines changed

9 files changed

+257
-110
lines changed

CHANGELOG.md

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

55
### Features
66

7+
- Add contextTags support for logging integrations ([#4783](https://github.com/getsentry/sentry-java/pull/4783))
8+
- MDC/ThreadContext contextTags are now extracted and attached as attributes to Sentry logs for Logback, Log4j2, and JUL integrations
9+
- Works with the existing `contextTags` option (e.g., `options.addContextTag("userId")`)
10+
- Centralizes contextTags processing logic in new `ContextTagsUtil` class
711
- Add session replay id to Sentry Logs ([#4740](https://github.com/getsentry/sentry-java/pull/4740))
812
- Add support for continuous profiling of JVM applications on macOS and Linux ([#4556](https://github.com/getsentry/sentry-java/pull/4556))
913
- [Sentry continuous profiling](https://docs.sentry.io/product/explore/profiling/) on the JVM is using async-profiler under the hood.

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

Lines changed: 7 additions & 10 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.ContextTagsUtil;
2526
import java.text.MessageFormat;
2627
import java.util.ArrayList;
2728
import java.util.Date;
@@ -160,6 +161,11 @@ protected void captureLog(@NotNull LogRecord loggingEvent) {
160161
attributes.add(SentryAttribute.stringAttribute("sentry.message.template", message));
161162
}
162163

164+
final Map<String, String> mdcProperties = MDC.getMDCAdapter().getCopyOfContextMap();
165+
if (mdcProperties != null) {
166+
ContextTagsUtil.applyContextTagsToLogAttributes(attributes, mdcProperties);
167+
}
168+
163169
final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);
164170
params.setOrigin("auto.log.jul");
165171

@@ -312,16 +318,7 @@ SentryEvent createEvent(final @NotNull LogRecord record) {
312318
// get tags from ScopesAdapter options to allow getting the correct tags if Sentry has been
313319
// initialized somewhere else
314320
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-
}
321+
ContextTagsUtil.applyContextTagsToEvent(event, contextTags, mdcProperties);
325322
// put the rest of mdc tags in contexts
326323
if (!mdcProperties.isEmpty()) {
327324
event.getContexts().put("MDC", mdcProperties);

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,4 +555,29 @@ class SentryHandlerTest {
555555
}
556556
)
557557
}
558+
559+
@Test
560+
fun `sets contextTags from MDC as attributes on logs`() {
561+
fixture = Fixture(minimumLevel = Level.INFO, contextTags = listOf("sessionId", "operationId"))
562+
563+
MDC.put("sessionId", "session-123")
564+
MDC.put("operationId", "op-456")
565+
MDC.put("otherTag", "otherValue") // Should not be included in attributes
566+
fixture.logger.info("testing context tags in logs")
567+
568+
Sentry.flush(1000)
569+
570+
verify(fixture.transport)
571+
.send(
572+
checkLogs { logs ->
573+
val log = logs.items.first()
574+
assertEquals("testing context tags in logs", log.body)
575+
val attributes = log.attributes!!
576+
assertEquals("session-123", attributes["sessionId"]?.value)
577+
assertEquals("op-456", attributes["operationId"]?.value)
578+
assertNull(attributes["otherTag"]) // Should not be included as it's not in contextTags
579+
assertEquals("auto.log.jul", attributes["sentry.origin"]?.value)
580+
}
581+
)
582+
}
558583
}

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

Lines changed: 93 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@
2525
import io.sentry.protocol.Message;
2626
import io.sentry.protocol.SdkVersion;
2727
import io.sentry.util.CollectionUtils;
28+
import io.sentry.util.ContextTagsUtil;
29+
2830
import java.util.Arrays;
2931
import java.util.Collections;
3032
import java.util.List;
3133
import java.util.Map;
3234
import java.util.Objects;
3335
import java.util.Optional;
3436
import java.util.stream.Collectors;
37+
3538
import org.apache.logging.log4j.Level;
3639
import org.apache.logging.log4j.core.Filter;
3740
import org.apache.logging.log4j.core.LogEvent;
@@ -44,7 +47,9 @@
4447
import org.jetbrains.annotations.NotNull;
4548
import org.jetbrains.annotations.Nullable;
4649

47-
/** Appender for Log4j2 in charge of sending the logged events to a Sentry server. */
50+
/**
51+
* Appender for Log4j2 in charge of sending the logged events to a Sentry server.
52+
*/
4853
@Plugin(name = "Sentry", category = "Core", elementType = "appender", printObject = true)
4954
@Open
5055
public class SentryAppender extends AbstractAppender {
@@ -61,49 +66,49 @@ public class SentryAppender extends AbstractAppender {
6166

6267
static {
6368
SentryIntegrationPackageStorage.getInstance()
64-
.addPackage("maven:io.sentry:sentry-log4j2", BuildConfig.VERSION_NAME);
69+
.addPackage("maven:io.sentry:sentry-log4j2", BuildConfig.VERSION_NAME);
6570
}
6671

6772
/**
6873
* @deprecated This constructor is deprecated. Please use {@link #SentryAppender(String, Filter,
69-
* String, Level, Level, Level, Boolean, ITransportFactory, IScopes, String[])} instead.
74+
* String, Level, Level, Level, Boolean, ITransportFactory, IScopes, String[])} instead.
7075
*/
7176
@Deprecated
7277
@SuppressWarnings("InlineMeSuggester")
7378
public SentryAppender(
74-
final @NotNull String name,
75-
final @Nullable Filter filter,
76-
final @Nullable String dsn,
77-
final @Nullable Level minimumBreadcrumbLevel,
78-
final @Nullable Level minimumEventLevel,
79-
final @Nullable Boolean debug,
80-
final @Nullable ITransportFactory transportFactory,
81-
final @NotNull IScopes scopes,
82-
final @Nullable String[] contextTags) {
79+
final @NotNull String name,
80+
final @Nullable Filter filter,
81+
final @Nullable String dsn,
82+
final @Nullable Level minimumBreadcrumbLevel,
83+
final @Nullable Level minimumEventLevel,
84+
final @Nullable Boolean debug,
85+
final @Nullable ITransportFactory transportFactory,
86+
final @NotNull IScopes scopes,
87+
final @Nullable String[] contextTags) {
8388
this(
84-
name,
85-
filter,
86-
dsn,
87-
minimumBreadcrumbLevel,
88-
minimumEventLevel,
89-
null,
90-
debug,
91-
transportFactory,
92-
scopes,
93-
contextTags);
89+
name,
90+
filter,
91+
dsn,
92+
minimumBreadcrumbLevel,
93+
minimumEventLevel,
94+
null,
95+
debug,
96+
transportFactory,
97+
scopes,
98+
contextTags);
9499
}
95100

96101
public SentryAppender(
97-
final @NotNull String name,
98-
final @Nullable Filter filter,
99-
final @Nullable String dsn,
100-
final @Nullable Level minimumBreadcrumbLevel,
101-
final @Nullable Level minimumEventLevel,
102-
final @Nullable Level minimumLevel,
103-
final @Nullable Boolean debug,
104-
final @Nullable ITransportFactory transportFactory,
105-
final @NotNull IScopes scopes,
106-
final @Nullable String[] contextTags) {
102+
final @NotNull String name,
103+
final @Nullable Filter filter,
104+
final @Nullable String dsn,
105+
final @Nullable Level minimumBreadcrumbLevel,
106+
final @Nullable Level minimumEventLevel,
107+
final @Nullable Level minimumLevel,
108+
final @Nullable Boolean debug,
109+
final @Nullable ITransportFactory transportFactory,
110+
final @NotNull IScopes scopes,
111+
final @Nullable String[] contextTags) {
107112
super(name, filter, null, true, null);
108113
this.dsn = dsn;
109114
if (minimumBreadcrumbLevel != null) {
@@ -124,64 +129,64 @@ public SentryAppender(
124129
/**
125130
* Create a Sentry Appender.
126131
*
127-
* @param name The name of the Appender.
132+
* @param name The name of the Appender.
128133
* @param minimumBreadcrumbLevel The min. level of the breadcrumb.
129-
* @param minimumEventLevel The min. level of the event.
130-
* @param minimumLevel The min. level of the log event.
131-
* @param dsn the Sentry DSN.
132-
* @param debug if Sentry debug mode should be on
133-
* @param filter The filter, if any, to use.
134+
* @param minimumEventLevel The min. level of the event.
135+
* @param minimumLevel The min. level of the log event.
136+
* @param dsn the Sentry DSN.
137+
* @param debug if Sentry debug mode should be on
138+
* @param filter The filter, if any, to use.
134139
* @return The SentryAppender.
135140
*/
136141
@PluginFactory
137142
public static @Nullable SentryAppender createAppender(
138-
@Nullable @PluginAttribute("name") final String name,
139-
@Nullable @PluginAttribute("minimumBreadcrumbLevel") final Level minimumBreadcrumbLevel,
140-
@Nullable @PluginAttribute("minimumEventLevel") final Level minimumEventLevel,
141-
@Nullable @PluginAttribute("minimumLevel") final Level minimumLevel,
142-
@Nullable @PluginAttribute("dsn") final String dsn,
143-
@Nullable @PluginAttribute("debug") final Boolean debug,
144-
@Nullable @PluginElement("filter") final Filter filter,
145-
@Nullable @PluginAttribute("contextTags") final String contextTags) {
143+
@Nullable @PluginAttribute("name") final String name,
144+
@Nullable @PluginAttribute("minimumBreadcrumbLevel") final Level minimumBreadcrumbLevel,
145+
@Nullable @PluginAttribute("minimumEventLevel") final Level minimumEventLevel,
146+
@Nullable @PluginAttribute("minimumLevel") final Level minimumLevel,
147+
@Nullable @PluginAttribute("dsn") final String dsn,
148+
@Nullable @PluginAttribute("debug") final Boolean debug,
149+
@Nullable @PluginElement("filter") final Filter filter,
150+
@Nullable @PluginAttribute("contextTags") final String contextTags) {
146151

147152
if (name == null) {
148153
LOGGER.error("No name provided for SentryAppender");
149154
return null;
150155
}
151156
return new SentryAppender(
152-
name,
153-
filter,
154-
dsn,
155-
minimumBreadcrumbLevel,
156-
minimumEventLevel,
157-
minimumLevel,
158-
debug,
159-
null,
160-
ScopesAdapter.getInstance(),
161-
contextTags != null ? contextTags.split(",") : null);
157+
name,
158+
filter,
159+
dsn,
160+
minimumBreadcrumbLevel,
161+
minimumEventLevel,
162+
minimumLevel,
163+
debug,
164+
null,
165+
ScopesAdapter.getInstance(),
166+
contextTags != null ? contextTags.split(",") : null);
162167
}
163168

164169
@Override
165170
public void start() {
166171
try {
167172
Sentry.init(
168-
options -> {
169-
options.setEnableExternalConfiguration(true);
170-
options.setInitPriority(InitPriority.LOWEST);
171-
options.setDsn(dsn);
172-
if (debug != null) {
173-
options.setDebug(debug);
174-
}
175-
options.setSentryClientName(
176-
BuildConfig.SENTRY_LOG4J2_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
177-
options.setSdkVersion(createSdkVersion(options));
178-
if (contextTags != null) {
179-
for (final String contextTag : contextTags) {
180-
options.addContextTag(contextTag);
181-
}
173+
options -> {
174+
options.setEnableExternalConfiguration(true);
175+
options.setInitPriority(InitPriority.LOWEST);
176+
options.setDsn(dsn);
177+
if (debug != null) {
178+
options.setDebug(debug);
179+
}
180+
options.setSentryClientName(
181+
BuildConfig.SENTRY_LOG4J2_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
182+
options.setSdkVersion(createSdkVersion(options));
183+
if (contextTags != null) {
184+
for (final String contextTag : contextTags) {
185+
options.addContextTag(contextTag);
182186
}
183-
Optional.ofNullable(transportFactory).ifPresent(options::setTransportFactory);
184-
});
187+
}
188+
Optional.ofNullable(transportFactory).ifPresent(options::setTransportFactory);
189+
});
185190
} catch (IllegalArgumentException e) {
186191
LOGGER.warn("Failed to init Sentry during appender initialization: " + e.getMessage());
187192
}
@@ -192,7 +197,7 @@ public void start() {
192197
@Override
193198
public void append(final @NotNull LogEvent eventObject) {
194199
if (scopes.getOptions().getLogs().isEnabled()
195-
&& eventObject.getLevel().isMoreSpecificThan(minimumLevel)) {
200+
&& eventObject.getLevel().isMoreSpecificThan(minimumLevel)) {
196201
captureLog(eventObject);
197202
}
198203
if (eventObject.getLevel().isMoreSpecificThan(minimumEventLevel)) {
@@ -227,7 +232,12 @@ protected void captureLog(@NotNull LogEvent loggingEvent) {
227232

228233
if (nonFormattedMessage != null && !formattedMessage.equals(nonFormattedMessage)) {
229234
attributes.add(
230-
SentryAttribute.stringAttribute("sentry.message.template", nonFormattedMessage));
235+
SentryAttribute.stringAttribute("sentry.message.template", nonFormattedMessage));
236+
}
237+
238+
final Map<String, String> contextData = loggingEvent.getContextData().toMap();
239+
if (contextData != null) {
240+
ContextTagsUtil.applyContextTagsToLogAttributes(attributes, contextData);
231241
}
232242

233243
final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);
@@ -259,8 +269,8 @@ protected void captureLog(@NotNull LogEvent loggingEvent) {
259269
final Mechanism mechanism = new Mechanism();
260270
mechanism.setType(MECHANISM_TYPE);
261271
final Throwable mechanismException =
262-
new ExceptionMechanismException(
263-
mechanism, throwableInformation.getThrowable(), Thread.currentThread());
272+
new ExceptionMechanismException(
273+
mechanism, throwableInformation.getThrowable(), Thread.currentThread());
264274
event.setThrowable(mechanismException);
265275
}
266276

@@ -273,21 +283,14 @@ protected void captureLog(@NotNull LogEvent loggingEvent) {
273283
}
274284

275285
final Map<String, String> contextData =
276-
CollectionUtils.filterMapEntries(
277-
loggingEvent.getContextData().toMap(), entry -> entry.getValue() != null);
286+
CollectionUtils.filterMapEntries(
287+
loggingEvent.getContextData().toMap(), entry -> entry.getValue() != null);
278288
if (!contextData.isEmpty()) {
279289
// get tags from ScopesAdapter options to allow getting the correct tags if Sentry has been
280290
// initialized somewhere else
281291
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-
}
292+
if (contextTags != null) {
293+
ContextTagsUtil.applyContextTagsToEvent(event, contextTags, contextData);
291294
}
292295
// put the rest of mdc tags in contexts
293296
if (!contextData.isEmpty()) {
@@ -301,9 +304,9 @@ protected void captureLog(@NotNull LogEvent loggingEvent) {
301304
private @NotNull List<String> toParams(final @Nullable Object[] arguments) {
302305
if (arguments != null) {
303306
return Arrays.stream(arguments)
304-
.filter(Objects::nonNull)
305-
.map(Object::toString)
306-
.collect(Collectors.toList());
307+
.filter(Objects::nonNull)
308+
.map(Object::toString)
309+
.collect(Collectors.toList());
307310
} else {
308311
return Collections.emptyList();
309312
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,4 +591,31 @@ class SentryAppenderTest {
591591
}
592592
)
593593
}
594+
595+
@Test
596+
fun `sets contextTags from ThreadContext as attributes on logs`() {
597+
val logger =
598+
fixture.getSut(minimumLevel = Level.INFO, contextTags = listOf("traceId", "spanId"))
599+
ScopesAdapter.getInstance().options.logs.isEnabled = true
600+
601+
ThreadContext.put("traceId", "trace-123")
602+
ThreadContext.put("spanId", "span-456")
603+
ThreadContext.put("otherTag", "otherValue") // Should not be included in attributes
604+
logger.info("testing context tags in logs")
605+
606+
Sentry.flush(1000)
607+
608+
verify(fixture.transport)
609+
.send(
610+
checkLogs { logs ->
611+
val log = logs.items.first()
612+
assertEquals("testing context tags in logs", log.body)
613+
val attributes = log.attributes!!
614+
assertEquals("trace-123", attributes["traceId"]?.value)
615+
assertEquals("span-456", attributes["spanId"]?.value)
616+
assertNull(attributes["otherTag"]) // Should not be included as it's not in contextTags
617+
assertEquals("auto.log.log4j2", attributes["sentry.origin"]?.value)
618+
}
619+
)
620+
}
594621
}

0 commit comments

Comments
 (0)