Skip to content

Commit b93e18d

Browse files
authored
Send Logback logs to Sentry as logs (#4502)
* Add user id, username and email to log attributes * wip * Check log event count before sending envelope * changelog * Min event level config option; enable logs in logback sample * review feedback * treat unformatted message and params as pii if logback encoder is present * changelog
1 parent bef763e commit b93e18d

File tree

6 files changed

+158
-3
lines changed

6 files changed

+158
-3
lines changed

CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,40 @@
1111
- No longer send out empty log envelopes ([#4497](https://github.com/getsentry/sentry-java/pull/4497))
1212
- Session Replay: Expand fix for crash on devices to all Unisoc/Spreadtrum chipsets ([#4510](https://github.com/getsentry/sentry-java/pull/4510))
1313

14+
### Features
15+
16+
- Send Logback logs to Sentry as logs ([#4502](https://github.com/getsentry/sentry-java/pull/4502))
17+
- You need to enable the logs feature and can also set the `minimumLevel` for log events:
18+
```xml
19+
<appender name="sentry" class="io.sentry.logback.SentryAppender">
20+
<options>
21+
<!-- NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard -->
22+
<dsn>https://[email protected]/5428563</dsn>
23+
<logs>
24+
<enabled>true</enabled>
25+
</logs>
26+
</options>
27+
<!-- Demonstrates how to modify the minimum values -->
28+
<!-- Default for Events is ERROR -->
29+
<minimumEventLevel>WARN</minimumEventLevel>
30+
<!-- Default for Breadcrumbs is INFO -->
31+
<minimumBreadcrumbLevel>DEBUG</minimumBreadcrumbLevel>
32+
<!-- Default for Breadcrumbs is INFO -->
33+
<minimumLevel>INFO</minimumLevel>
34+
</appender>
35+
```
36+
- If you manually initialize Sentry, you may also enable logs on `Sentry.init`:
37+
```java
38+
Sentry.init(options -> {
39+
...
40+
options.getLogs().setEnabled(true);
41+
});
42+
```
43+
- Enabling via `sentry.properties` is also possible:
44+
```properties
45+
logs.enabled=true
46+
```
47+
1448
### Dependencies
1549

1650
- Bump Gradle from v8.14.1 to v8.14.2 ([#4473](https://github.com/getsentry/sentry-java/pull/4473))

sentry-logback/api/sentry-logback.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ public class io/sentry/logback/SentryAppender : ch/qos/logback/core/Unsynchroniz
88
public fun <init> ()V
99
protected fun append (Lch/qos/logback/classic/spi/ILoggingEvent;)V
1010
protected synthetic fun append (Ljava/lang/Object;)V
11+
protected fun captureLog (Lch/qos/logback/classic/spi/ILoggingEvent;)V
1112
protected fun createBreadcrumb (Lch/qos/logback/classic/spi/ILoggingEvent;)Lio/sentry/Breadcrumb;
1213
protected fun createEvent (Lch/qos/logback/classic/spi/ILoggingEvent;)Lio/sentry/SentryEvent;
1314
public fun getMinimumBreadcrumbLevel ()Lch/qos/logback/classic/Level;
1415
public fun getMinimumEventLevel ()Lch/qos/logback/classic/Level;
16+
public fun getMinimumLevel ()Lch/qos/logback/classic/Level;
1517
public fun setEncoder (Lch/qos/logback/core/encoder/Encoder;)V
1618
public fun setMinimumBreadcrumbLevel (Lch/qos/logback/classic/Level;)V
1719
public fun setMinimumEventLevel (Lch/qos/logback/classic/Level;)V
20+
public fun setMinimumLevel (Lch/qos/logback/classic/Level;)V
1821
public fun setOptions (Lio/sentry/SentryOptions;)V
1922
public fun start ()V
2023
}

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616
import io.sentry.InitPriority;
1717
import io.sentry.ScopesAdapter;
1818
import io.sentry.Sentry;
19+
import io.sentry.SentryAttribute;
20+
import io.sentry.SentryAttributes;
1921
import io.sentry.SentryEvent;
2022
import io.sentry.SentryIntegrationPackageStorage;
2123
import io.sentry.SentryLevel;
24+
import io.sentry.SentryLogLevel;
2225
import io.sentry.SentryOptions;
2326
import io.sentry.exception.ExceptionMechanismException;
27+
import io.sentry.logger.SentryLogParameters;
2428
import io.sentry.protocol.Mechanism;
2529
import io.sentry.protocol.Message;
2630
import io.sentry.protocol.SdkVersion;
@@ -46,6 +50,7 @@ public class SentryAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
4650
private @Nullable ITransportFactory transportFactory;
4751
private @NotNull Level minimumBreadcrumbLevel = Level.INFO;
4852
private @NotNull Level minimumEventLevel = Level.ERROR;
53+
private @NotNull Level minimumLevel = Level.INFO;
4954
private @Nullable Encoder<ILoggingEvent> encoder;
5055

5156
static {
@@ -78,6 +83,9 @@ public void start() {
7883

7984
@Override
8085
protected void append(@NotNull ILoggingEvent eventObject) {
86+
if (options.getLogs().isEnabled() && eventObject.getLevel().isGreaterOrEqual(minimumLevel)) {
87+
captureLog(eventObject);
88+
}
8189
if (eventObject.getLevel().isGreaterOrEqual(minimumEventLevel)) {
8290
final Hint hint = new Hint();
8391
hint.set(SENTRY_SYNTHETIC_EXCEPTION, eventObject);
@@ -162,6 +170,32 @@ protected void append(@NotNull ILoggingEvent eventObject) {
162170
return event;
163171
}
164172

173+
/**
174+
* Captures a Sentry log from Logback's {@link ILoggingEvent}.
175+
*
176+
* @param loggingEvent the logback event
177+
*/
178+
// for the Android compatibility we must use old Java Date class
179+
@SuppressWarnings("JdkObsolete")
180+
protected void captureLog(@NotNull ILoggingEvent loggingEvent) {
181+
final @NotNull SentryLogLevel sentryLevel = toSentryLogLevel(loggingEvent.getLevel());
182+
183+
@Nullable Object[] arguments = null;
184+
final @NotNull SentryAttributes attributes = SentryAttributes.of();
185+
186+
// if encoder is set we treat message+params as PII as encoders may be used to mask/strip PII
187+
if (encoder == null || options.isSendDefaultPii()) {
188+
attributes.add(
189+
SentryAttribute.stringAttribute("sentry.message.template", loggingEvent.getMessage()));
190+
arguments = loggingEvent.getArgumentArray();
191+
}
192+
193+
final @NotNull String formattedMessage = formatted(loggingEvent);
194+
final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);
195+
196+
Sentry.logger().log(sentryLevel, params, formattedMessage, arguments);
197+
}
198+
165199
private String formatted(@NotNull ILoggingEvent loggingEvent) {
166200
if (encoder != null) {
167201
try {
@@ -218,6 +252,26 @@ private String formatted(@NotNull ILoggingEvent loggingEvent) {
218252
}
219253
}
220254

255+
/**
256+
* Transforms a {@link Level} into an {@link SentryLogLevel}.
257+
*
258+
* @param level original level as defined in log4j.
259+
* @return log level used within sentry.
260+
*/
261+
private static @NotNull SentryLogLevel toSentryLogLevel(@NotNull Level level) {
262+
if (level.isGreaterOrEqual(Level.ERROR)) {
263+
return SentryLogLevel.ERROR;
264+
} else if (level.isGreaterOrEqual(Level.WARN)) {
265+
return SentryLogLevel.WARN;
266+
} else if (level.isGreaterOrEqual(Level.INFO)) {
267+
return SentryLogLevel.INFO;
268+
} else if (level.isGreaterOrEqual(Level.DEBUG)) {
269+
return SentryLogLevel.DEBUG;
270+
} else {
271+
return SentryLogLevel.TRACE;
272+
}
273+
}
274+
221275
private @NotNull SdkVersion createSdkVersion(@NotNull SentryOptions sentryOptions) {
222276
SdkVersion sdkVersion = sentryOptions.getSdkVersion();
223277

@@ -258,6 +312,16 @@ public void setMinimumEventLevel(final @Nullable Level minimumEventLevel) {
258312
return minimumEventLevel;
259313
}
260314

315+
public void setMinimumLevel(final @Nullable Level minimumLevel) {
316+
if (minimumLevel != null) {
317+
this.minimumLevel = minimumLevel;
318+
}
319+
}
320+
321+
public @NotNull Level getMinimumLevel() {
322+
return minimumLevel;
323+
}
324+
261325
@ApiStatus.Internal
262326
void setTransportFactory(final @Nullable ITransportFactory transportFactory) {
263327
this.transportFactory = transportFactory;

sentry-samples/sentry-samples-logback/src/main/resources/logback.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@
1313
<dsn>https://[email protected]/5428563</dsn>
1414
<contextTag>userId</contextTag>
1515
<contextTag>requestId</contextTag>
16+
<logs>
17+
<enabled>true</enabled>
18+
</logs>
1619
</options>
1720
<!-- Demonstrates how to modify the minimum values -->
1821
<!-- Default for Events is ERROR -->
1922
<minimumEventLevel>WARN</minimumEventLevel>
2023
<!-- Default for Breadcrumbs is INFO -->
2124
<minimumBreadcrumbLevel>DEBUG</minimumBreadcrumbLevel>
25+
<!-- Default for Breadcrumbs is INFO -->
26+
<minimumLevel>INFO</minimumLevel>
2227
</appender>
2328

2429
<!-- it's important to set logger level to equal or lower than minimumBreadcrumbLevel and minimumEventLevel -->

sentry/src/main/java/io/sentry/logger/LoggerApi.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,11 @@ private void captureLog(
184184
i++;
185185
}
186186
if (i > 0) {
187-
attributes.put(
188-
"sentry.message.template",
189-
new SentryLogEventAttributeValue(SentryAttributeType.STRING, message));
187+
if (attributes.get("sentry.message.template") == null) {
188+
attributes.put(
189+
"sentry.message.template",
190+
new SentryLogEventAttributeValue(SentryAttributeType.STRING, message));
191+
}
190192
}
191193
}
192194

sentry/src/test/java/io/sentry/ScopesTest.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2880,6 +2880,53 @@ class ScopesTest {
28802880
)
28812881
}
28822882

2883+
@Test
2884+
fun `missing user does not break attributes`() {
2885+
val (sut, mockClient) =
2886+
getEnabledScopes {
2887+
it.logs.isEnabled = true
2888+
it.isSendDefaultPii = true
2889+
}
2890+
2891+
sut.logger().log(SentryLogLevel.WARN, "log message")
2892+
2893+
verify(mockClient)
2894+
.captureLog(
2895+
check {
2896+
assertEquals("log message", it.body)
2897+
2898+
assertNull(it.attributes?.get("user.id"))
2899+
assertNull(it.attributes?.get("user.name"))
2900+
assertNull(it.attributes?.get("user.email"))
2901+
},
2902+
anyOrNull(),
2903+
)
2904+
}
2905+
2906+
@Test
2907+
fun `missing user fields do not break attributes`() {
2908+
val (sut, mockClient) =
2909+
getEnabledScopes {
2910+
it.logs.isEnabled = true
2911+
it.isSendDefaultPii = true
2912+
}
2913+
2914+
sut.configureScope { scope -> scope.user = User() }
2915+
sut.logger().log(SentryLogLevel.WARN, "log message")
2916+
2917+
verify(mockClient)
2918+
.captureLog(
2919+
check {
2920+
assertEquals("log message", it.body)
2921+
2922+
assertNull(it.attributes?.get("user.id"))
2923+
assertNull(it.attributes?.get("user.name"))
2924+
assertNull(it.attributes?.get("user.email"))
2925+
},
2926+
anyOrNull(),
2927+
)
2928+
}
2929+
28832930
// endregion
28842931

28852932
@Test

0 commit comments

Comments
 (0)