Skip to content

Commit 6796acc

Browse files
authored
Send JUL logs to Sentry as logs (#4518)
* Send JUL logs to Sentry as logs * changelog * fix gh issue number
1 parent 12f6380 commit 6796acc

File tree

8 files changed

+181
-1
lines changed

8 files changed

+181
-1
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Send JUL logs to Sentry as logs ([#4518](https://github.com/getsentry/sentry-java/pull/4518))
8+
- You need to enable the logs feature, either in `sentry.properties`:
9+
```properties
10+
logs.enabled=true
11+
```
12+
- Or, if you manually initialize Sentry, you may also enable logs on `Sentry.init`:
13+
```java
14+
Sentry.init(options -> {
15+
...
16+
options.getLogs().setEnabled(true);
17+
});
18+
```
19+
- It is also possible to set the `minimumLevel` in `logging.properties`, meaning any log message >= the configured level will be sent to Sentry and show up under Logs:
20+
```properties
21+
io.sentry.jul.SentryHandler.minimumLevel=CONFIG
22+
```
23+
324
## 8.15.1
425

526
### Fixes

sentry-jul/api/sentry-jul.api

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ public class io/sentry/jul/SentryHandler : java/util/logging/Handler {
99
public fun <init> ()V
1010
public fun <init> (Lio/sentry/SentryOptions;)V
1111
public fun <init> (Lio/sentry/SentryOptions;Z)V
12+
protected fun captureLog (Ljava/util/logging/LogRecord;)V
1213
public fun close ()V
1314
public fun flush ()V
1415
public fun getMinimumBreadcrumbLevel ()Ljava/util/logging/Level;
1516
public fun getMinimumEventLevel ()Ljava/util/logging/Level;
17+
public fun getMinimumLevel ()Ljava/util/logging/Level;
1618
public fun isPrintfStyle ()Z
1719
public fun publish (Ljava/util/logging/LogRecord;)V
1820
public fun setMinimumBreadcrumbLevel (Ljava/util/logging/Level;)V
1921
public fun setMinimumEventLevel (Ljava/util/logging/Level;)V
22+
public fun setMinimumLevel (Ljava/util/logging/Level;)V
2023
public fun setPrintfStyle (Z)V
2124
}
2225

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@
99
import io.sentry.InitPriority;
1010
import io.sentry.ScopesAdapter;
1111
import io.sentry.Sentry;
12+
import io.sentry.SentryAttribute;
13+
import io.sentry.SentryAttributes;
1214
import io.sentry.SentryEvent;
1315
import io.sentry.SentryIntegrationPackageStorage;
1416
import io.sentry.SentryLevel;
17+
import io.sentry.SentryLogLevel;
1518
import io.sentry.SentryOptions;
1619
import io.sentry.exception.ExceptionMechanismException;
20+
import io.sentry.logger.SentryLogParameters;
1721
import io.sentry.protocol.Mechanism;
1822
import io.sentry.protocol.Message;
1923
import io.sentry.protocol.SdkVersion;
@@ -50,6 +54,7 @@ public class SentryHandler extends Handler {
5054

5155
private @NotNull Level minimumBreadcrumbLevel = Level.INFO;
5256
private @NotNull Level minimumEventLevel = Level.SEVERE;
57+
private @NotNull Level minimumLevel = Level.INFO;
5358

5459
static {
5560
SentryIntegrationPackageStorage.getInstance()
@@ -106,6 +111,9 @@ public void publish(final @NotNull LogRecord record) {
106111
return;
107112
}
108113
try {
114+
if (record.getLevel().intValue() >= minimumLevel.intValue()) {
115+
captureLog(record);
116+
}
109117
if (record.getLevel().intValue() >= minimumEventLevel.intValue()) {
110118
final Hint hint = new Hint();
111119
hint.set(SENTRY_SYNTHETIC_EXCEPTION, record);
@@ -126,6 +134,46 @@ public void publish(final @NotNull LogRecord record) {
126134
}
127135
}
128136

137+
/**
138+
* Captures a Sentry log from JULs {@link LogRecord}.
139+
*
140+
* @param loggingEvent the JUL log record
141+
*/
142+
// for the Android compatibility we must use old Java Date class
143+
@SuppressWarnings("JdkObsolete")
144+
protected void captureLog(@NotNull LogRecord loggingEvent) {
145+
final @NotNull SentryLogLevel sentryLevel = toSentryLogLevel(loggingEvent.getLevel());
146+
147+
final @Nullable Object[] arguments = loggingEvent.getParameters();
148+
final @NotNull SentryAttributes attributes = SentryAttributes.of();
149+
150+
@NotNull String message = loggingEvent.getMessage();
151+
if (loggingEvent.getResourceBundle() != null
152+
&& loggingEvent.getResourceBundle().containsKey(loggingEvent.getMessage())) {
153+
message = loggingEvent.getResourceBundle().getString(loggingEvent.getMessage());
154+
}
155+
156+
attributes.add(SentryAttribute.stringAttribute("sentry.message.template", message));
157+
158+
final @NotNull String formattedMessage = maybeFormatted(arguments, message);
159+
final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);
160+
161+
Sentry.logger().log(sentryLevel, params, formattedMessage, arguments);
162+
}
163+
164+
private @NotNull String maybeFormatted(
165+
final @NotNull Object[] arguments, final @NotNull String message) {
166+
if (arguments != null) {
167+
try {
168+
return formatMessage(message, arguments);
169+
} catch (RuntimeException e) {
170+
// local formatting failed, sending raw message instead of formatted message
171+
}
172+
}
173+
174+
return message;
175+
}
176+
129177
/** Retrieves the properties of the logger. */
130178
private void retrieveProperties() {
131179
final LogManager manager = LogManager.getLogManager();
@@ -141,6 +189,10 @@ private void retrieveProperties() {
141189
if (minimumEventLevel != null) {
142190
setMinimumEventLevel(parseLevelOrDefault(minimumEventLevel));
143191
}
192+
final String minimumLevel = manager.getProperty(className + ".minimumLevel");
193+
if (minimumLevel != null) {
194+
setMinimumLevel(parseLevelOrDefault(minimumLevel));
195+
}
144196
}
145197

146198
/**
@@ -163,6 +215,26 @@ private void retrieveProperties() {
163215
}
164216
}
165217

218+
/**
219+
* Transforms a {@link Level} into an {@link SentryLogLevel}.
220+
*
221+
* @param level original level as defined in JUL.
222+
* @return log level used within sentry logs.
223+
*/
224+
private static @NotNull SentryLogLevel toSentryLogLevel(final @NotNull Level level) {
225+
if (level.intValue() >= Level.SEVERE.intValue()) {
226+
return SentryLogLevel.ERROR;
227+
} else if (level.intValue() >= Level.WARNING.intValue()) {
228+
return SentryLogLevel.WARN;
229+
} else if (level.intValue() >= Level.INFO.intValue()) {
230+
return SentryLogLevel.INFO;
231+
} else if (level.intValue() >= Level.FINE.intValue()) {
232+
return SentryLogLevel.DEBUG;
233+
} else {
234+
return SentryLogLevel.TRACE;
235+
}
236+
}
237+
166238
private @NotNull Level parseLevelOrDefault(final @NotNull String levelName) {
167239
try {
168240
return Level.parse(levelName.trim());
@@ -339,6 +411,16 @@ public void setMinimumEventLevel(final @Nullable Level minimumEventLevel) {
339411
return minimumEventLevel;
340412
}
341413

414+
public void setMinimumLevel(final @Nullable Level minimumLevel) {
415+
if (minimumLevel != null) {
416+
this.minimumLevel = minimumLevel;
417+
}
418+
}
419+
420+
public @NotNull Level getMinimumLevel() {
421+
return minimumLevel;
422+
}
423+
342424
public boolean isPrintfStyle() {
343425
return printfStyle;
344426
}

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package io.sentry.jul
33
import io.sentry.InitPriority
44
import io.sentry.Sentry
55
import io.sentry.SentryLevel
6+
import io.sentry.SentryLogLevel
67
import io.sentry.SentryOptions
78
import io.sentry.checkEvent
9+
import io.sentry.checkLogs
810
import io.sentry.test.initForTest
911
import io.sentry.transport.ITransport
1012
import java.time.Instant
@@ -29,6 +31,7 @@ class SentryHandlerTest {
2931
private class Fixture(
3032
minimumBreadcrumbLevel: Level? = null,
3133
minimumEventLevel: Level? = null,
34+
minimumLevel: Level? = null,
3235
val configureWithLogManager: Boolean = false,
3336
val transport: ITransport = mock(),
3437
contextTags: List<String>? = null,
@@ -45,6 +48,7 @@ class SentryHandlerTest {
4548
handler = SentryHandler(options, configureWithLogManager, true)
4649
handler.setMinimumBreadcrumbLevel(minimumBreadcrumbLevel)
4750
handler.setMinimumEventLevel(minimumEventLevel)
51+
handler.setMinimumLevel(minimumLevel)
4852
handler.level = Level.ALL
4953
logger.handlers.forEach { logger.removeHandler(it) }
5054
logger.addHandler(handler)
@@ -401,4 +405,70 @@ class SentryHandlerTest {
401405
anyOrNull(),
402406
)
403407
}
408+
409+
@Test
410+
fun `converts finest log level to Sentry log level`() {
411+
fixture = Fixture(minimumLevel = Level.FINEST)
412+
fixture.logger.finest("testing trace level")
413+
414+
Sentry.flush(1000)
415+
416+
verify(fixture.transport)
417+
.send(checkLogs { event -> assertEquals(SentryLogLevel.TRACE, event.items.first().level) })
418+
}
419+
420+
@Test
421+
fun `converts fine log level to Sentry log level`() {
422+
fixture = Fixture(minimumLevel = Level.FINE)
423+
fixture.logger.fine("testing trace level")
424+
425+
Sentry.flush(1000)
426+
427+
verify(fixture.transport)
428+
.send(checkLogs { event -> assertEquals(SentryLogLevel.DEBUG, event.items.first().level) })
429+
}
430+
431+
@Test
432+
fun `converts config log level to Sentry log level`() {
433+
fixture = Fixture(minimumLevel = Level.CONFIG)
434+
fixture.logger.config("testing debug level")
435+
436+
Sentry.flush(1000)
437+
438+
verify(fixture.transport)
439+
.send(checkLogs { event -> assertEquals(SentryLogLevel.DEBUG, event.items.first().level) })
440+
}
441+
442+
@Test
443+
fun `converts info log level to Sentry log level`() {
444+
fixture = Fixture(minimumLevel = Level.INFO)
445+
fixture.logger.info("testing info level")
446+
447+
Sentry.flush(1000)
448+
449+
verify(fixture.transport)
450+
.send(checkLogs { event -> assertEquals(SentryLogLevel.INFO, event.items.first().level) })
451+
}
452+
453+
@Test
454+
fun `converts warn log level to Sentry log level`() {
455+
fixture = Fixture(minimumLevel = Level.WARNING)
456+
fixture.logger.warning("testing warn level")
457+
458+
Sentry.flush(1000)
459+
460+
verify(fixture.transport)
461+
.send(checkLogs { event -> assertEquals(SentryLogLevel.WARN, event.items.first().level) })
462+
}
463+
464+
@Test
465+
fun `converts severe log level to Sentry log level`() {
466+
fixture = Fixture(minimumLevel = Level.SEVERE)
467+
fixture.logger.severe("testing error level")
468+
469+
Sentry.flush(1000)
470+
471+
verify(fixture.transport)
472+
.send(checkLogs { event -> assertEquals(SentryLogLevel.ERROR, event.items.first().level) })
473+
}
404474
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
io.sentry.jul.SentryHandler.level=ALL
22
io.sentry.jul.SentryHandler.minimumEventLevel=WARNING
33
io.sentry.jul.SentryHandler.minimumBreadcrumbLevel=CONFIG
4+
io.sentry.jul.SentryHandler.minimumLevel=CONFIG
45
io.sentry.jul.SentryHandler.printfStyle=true
56

67
jul.SentryHandlerTest.handlers=java.util.logging.ConsoleHandler, io.sentry.jul.SentryHandler
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
release=release from sentry.properties
2+
logs.enabled=true

sentry-samples/sentry-samples-jul/src/main/resources/logging.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
io.sentry.jul.SentryHandler.minimumEventLevel=DEBUG
1+
io.sentry.jul.SentryHandler.minimumEventLevel=FINE
22
io.sentry.jul.SentryHandler.minimumBreadcrumbLevel=CONFIG
3+
io.sentry.jul.SentryHandler.minimumLevel=FINE
34
io.sentry.jul.SentryHandler.printfStyle=true
45
io.sentry.jul.SentryHandler.level=CONFIG
56
handlers=io.sentry.jul.SentryHandler

sentry-samples/sentry-samples-jul/src/main/resources/sentry.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ debug=true
44
environment=staging
55
in-app-includes=io.sentry.samples
66
context-tags=userId,requestId
7+
logs.enabled=true

0 commit comments

Comments
 (0)