Skip to content

Send JUL logs to Sentry as logs #4518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 26, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions sentry-jul/api/sentry-jul.api
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ public class io/sentry/jul/SentryHandler : java/util/logging/Handler {
public fun <init> ()V
public fun <init> (Lio/sentry/SentryOptions;)V
public fun <init> (Lio/sentry/SentryOptions;Z)V
protected fun captureLog (Ljava/util/logging/LogRecord;)V
public fun close ()V
public fun flush ()V
public fun getMinimumBreadcrumbLevel ()Ljava/util/logging/Level;
public fun getMinimumEventLevel ()Ljava/util/logging/Level;
public fun getMinimumLevel ()Ljava/util/logging/Level;
public fun isPrintfStyle ()Z
public fun publish (Ljava/util/logging/LogRecord;)V
public fun setMinimumBreadcrumbLevel (Ljava/util/logging/Level;)V
public fun setMinimumEventLevel (Ljava/util/logging/Level;)V
public fun setMinimumLevel (Ljava/util/logging/Level;)V
public fun setPrintfStyle (Z)V
}

82 changes: 82 additions & 0 deletions sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@
import io.sentry.InitPriority;
import io.sentry.ScopesAdapter;
import io.sentry.Sentry;
import io.sentry.SentryAttribute;
import io.sentry.SentryAttributes;
import io.sentry.SentryEvent;
import io.sentry.SentryIntegrationPackageStorage;
import io.sentry.SentryLevel;
import io.sentry.SentryLogLevel;
import io.sentry.SentryOptions;
import io.sentry.exception.ExceptionMechanismException;
import io.sentry.logger.SentryLogParameters;
import io.sentry.protocol.Mechanism;
import io.sentry.protocol.Message;
import io.sentry.protocol.SdkVersion;
Expand Down Expand Up @@ -50,6 +54,7 @@ public class SentryHandler extends Handler {

private @NotNull Level minimumBreadcrumbLevel = Level.INFO;
private @NotNull Level minimumEventLevel = Level.SEVERE;
private @NotNull Level minimumLevel = Level.INFO;

static {
SentryIntegrationPackageStorage.getInstance()
Expand Down Expand Up @@ -106,6 +111,9 @@ public void publish(final @NotNull LogRecord record) {
return;
}
try {
if (record.getLevel().intValue() >= minimumLevel.intValue()) {
captureLog(record);
}
if (record.getLevel().intValue() >= minimumEventLevel.intValue()) {
final Hint hint = new Hint();
hint.set(SENTRY_SYNTHETIC_EXCEPTION, record);
Expand All @@ -126,6 +134,46 @@ public void publish(final @NotNull LogRecord record) {
}
}

/**
* Captures a Sentry log from JULs {@link LogRecord}.
*
* @param loggingEvent the JUL log record
*/
// for the Android compatibility we must use old Java Date class
@SuppressWarnings("JdkObsolete")
protected void captureLog(@NotNull LogRecord loggingEvent) {
final @NotNull SentryLogLevel sentryLevel = toSentryLogLevel(loggingEvent.getLevel());

final @Nullable Object[] arguments = loggingEvent.getParameters();
final @NotNull SentryAttributes attributes = SentryAttributes.of();

@NotNull String message = loggingEvent.getMessage();
if (loggingEvent.getResourceBundle() != null
&& loggingEvent.getResourceBundle().containsKey(loggingEvent.getMessage())) {
message = loggingEvent.getResourceBundle().getString(loggingEvent.getMessage());
}

attributes.add(SentryAttribute.stringAttribute("sentry.message.template", message));

final @NotNull String formattedMessage = maybeFormatted(arguments, message);
final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);

Sentry.logger().log(sentryLevel, params, formattedMessage, arguments);
}

private @NotNull String maybeFormatted(
final @NotNull Object[] arguments, final @NotNull String message) {
if (arguments != null) {
try {
return formatMessage(message, arguments);
} catch (RuntimeException e) {
// local formatting failed, sending raw message instead of formatted message
}
}

return message;
}

/** Retrieves the properties of the logger. */
private void retrieveProperties() {
final LogManager manager = LogManager.getLogManager();
Expand All @@ -141,6 +189,10 @@ private void retrieveProperties() {
if (minimumEventLevel != null) {
setMinimumEventLevel(parseLevelOrDefault(minimumEventLevel));
}
final String minimumLevel = manager.getProperty(className + ".minimumLevel");
if (minimumLevel != null) {
setMinimumLevel(parseLevelOrDefault(minimumLevel));
}
}

/**
Expand All @@ -163,6 +215,26 @@ private void retrieveProperties() {
}
}

/**
* Transforms a {@link Level} into an {@link SentryLogLevel}.
*
* @param level original level as defined in JUL.
* @return log level used within sentry logs.
*/
private static @NotNull SentryLogLevel toSentryLogLevel(final @NotNull Level level) {
if (level.intValue() >= Level.SEVERE.intValue()) {
return SentryLogLevel.ERROR;
} else if (level.intValue() >= Level.WARNING.intValue()) {
return SentryLogLevel.WARN;
} else if (level.intValue() >= Level.INFO.intValue()) {
return SentryLogLevel.INFO;
} else if (level.intValue() >= Level.FINE.intValue()) {
return SentryLogLevel.DEBUG;
} else {
return SentryLogLevel.TRACE;
}
}

private @NotNull Level parseLevelOrDefault(final @NotNull String levelName) {
try {
return Level.parse(levelName.trim());
Expand Down Expand Up @@ -339,6 +411,16 @@ public void setMinimumEventLevel(final @Nullable Level minimumEventLevel) {
return minimumEventLevel;
}

public void setMinimumLevel(final @Nullable Level minimumLevel) {
if (minimumLevel != null) {
this.minimumLevel = minimumLevel;
}
}

public @NotNull Level getMinimumLevel() {
return minimumLevel;
}

public boolean isPrintfStyle() {
return printfStyle;
}
Expand Down
70 changes: 70 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 @@ -3,8 +3,10 @@ package io.sentry.jul
import io.sentry.InitPriority
import io.sentry.Sentry
import io.sentry.SentryLevel
import io.sentry.SentryLogLevel
import io.sentry.SentryOptions
import io.sentry.checkEvent
import io.sentry.checkLogs
import io.sentry.test.initForTest
import io.sentry.transport.ITransport
import java.time.Instant
Expand All @@ -29,6 +31,7 @@ class SentryHandlerTest {
private class Fixture(
minimumBreadcrumbLevel: Level? = null,
minimumEventLevel: Level? = null,
minimumLevel: Level? = null,
val configureWithLogManager: Boolean = false,
val transport: ITransport = mock(),
contextTags: List<String>? = null,
Expand All @@ -45,6 +48,7 @@ class SentryHandlerTest {
handler = SentryHandler(options, configureWithLogManager, true)
handler.setMinimumBreadcrumbLevel(minimumBreadcrumbLevel)
handler.setMinimumEventLevel(minimumEventLevel)
handler.setMinimumLevel(minimumLevel)
handler.level = Level.ALL
logger.handlers.forEach { logger.removeHandler(it) }
logger.addHandler(handler)
Expand Down Expand Up @@ -401,4 +405,70 @@ class SentryHandlerTest {
anyOrNull(),
)
}

@Test
fun `converts finest log level to Sentry log level`() {
fixture = Fixture(minimumLevel = Level.FINEST)
fixture.logger.finest("testing trace level")

Sentry.flush(1000)

verify(fixture.transport)
.send(checkLogs { event -> assertEquals(SentryLogLevel.TRACE, event.items.first().level) })
}

@Test
fun `converts fine log level to Sentry log level`() {
fixture = Fixture(minimumLevel = Level.FINE)
fixture.logger.fine("testing trace level")

Sentry.flush(1000)

verify(fixture.transport)
.send(checkLogs { event -> assertEquals(SentryLogLevel.DEBUG, event.items.first().level) })
}

@Test
fun `converts config log level to Sentry log level`() {
fixture = Fixture(minimumLevel = Level.CONFIG)
fixture.logger.config("testing debug level")

Sentry.flush(1000)

verify(fixture.transport)
.send(checkLogs { event -> assertEquals(SentryLogLevel.DEBUG, event.items.first().level) })
}

@Test
fun `converts info log level to Sentry log level`() {
fixture = Fixture(minimumLevel = Level.INFO)
fixture.logger.info("testing info level")

Sentry.flush(1000)

verify(fixture.transport)
.send(checkLogs { event -> assertEquals(SentryLogLevel.INFO, event.items.first().level) })
}

@Test
fun `converts warn log level to Sentry log level`() {
fixture = Fixture(minimumLevel = Level.WARNING)
fixture.logger.warning("testing warn level")

Sentry.flush(1000)

verify(fixture.transport)
.send(checkLogs { event -> assertEquals(SentryLogLevel.WARN, event.items.first().level) })
}

@Test
fun `converts severe log level to Sentry log level`() {
fixture = Fixture(minimumLevel = Level.SEVERE)
fixture.logger.severe("testing error level")

Sentry.flush(1000)

verify(fixture.transport)
.send(checkLogs { event -> assertEquals(SentryLogLevel.ERROR, event.items.first().level) })
}
}
1 change: 1 addition & 0 deletions sentry-jul/src/test/resources/logging.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
io.sentry.jul.SentryHandler.level=ALL
io.sentry.jul.SentryHandler.minimumEventLevel=WARNING
io.sentry.jul.SentryHandler.minimumBreadcrumbLevel=CONFIG
io.sentry.jul.SentryHandler.minimumLevel=CONFIG
io.sentry.jul.SentryHandler.printfStyle=true

jul.SentryHandlerTest.handlers=java.util.logging.ConsoleHandler, io.sentry.jul.SentryHandler
1 change: 1 addition & 0 deletions sentry-jul/src/test/resources/sentry.properties
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
release=release from sentry.properties
logs.enabled=true
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
io.sentry.jul.SentryHandler.minimumEventLevel=DEBUG
io.sentry.jul.SentryHandler.minimumEventLevel=FINE
io.sentry.jul.SentryHandler.minimumBreadcrumbLevel=CONFIG
io.sentry.jul.SentryHandler.minimumLevel=FINE
io.sentry.jul.SentryHandler.printfStyle=true
io.sentry.jul.SentryHandler.level=CONFIG
handlers=io.sentry.jul.SentryHandler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ debug=true
environment=staging
in-app-includes=io.sentry.samples
context-tags=userId,requestId
logs.enabled=true
Loading