diff --git a/CHANGELOG.md b/CHANGELOG.md
index dcc4c348b7..383841903f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,27 @@
```properties
io.sentry.jul.SentryHandler.minimumLevel=CONFIG
```
+- Send Log4j2 logs to Sentry as logs ([#4517](https://github.com/getsentry/sentry-java/pull/4517))
+ - You need to enable the logs feature either in `sentry.properties`:
+ ```properties
+ logs.enabled=true
+ ```
+ - If you manually initialize Sentry, you may also enable logs on `Sentry.init`:
+ ```java
+ Sentry.init(options -> {
+ ...
+ options.getLogs().setEnabled(true);
+ });
+ ```
+ - It is also possible to set the `minimumLevel` in `log4j2.xml`, meaning any log message >= the configured level will be sent to Sentry and show up under Logs:
+ ```xml
+
+ ```
## 8.15.1
diff --git a/sentry-log4j2/api/sentry-log4j2.api b/sentry-log4j2/api/sentry-log4j2.api
index b7fe8b3273..2a5d4bf789 100644
--- a/sentry-log4j2/api/sentry-log4j2.api
+++ b/sentry-log4j2/api/sentry-log4j2.api
@@ -6,8 +6,10 @@ public final class io/sentry/log4j2/BuildConfig {
public class io/sentry/log4j2/SentryAppender : org/apache/logging/log4j/core/appender/AbstractAppender {
public static final field MECHANISM_TYPE Ljava/lang/String;
public fun (Ljava/lang/String;Lorg/apache/logging/log4j/core/Filter;Ljava/lang/String;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Ljava/lang/Boolean;Lio/sentry/ITransportFactory;Lio/sentry/IScopes;[Ljava/lang/String;)V
+ public fun (Ljava/lang/String;Lorg/apache/logging/log4j/core/Filter;Ljava/lang/String;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Ljava/lang/Boolean;Lio/sentry/ITransportFactory;Lio/sentry/IScopes;[Ljava/lang/String;)V
public fun append (Lorg/apache/logging/log4j/core/LogEvent;)V
- public static fun createAppender (Ljava/lang/String;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Ljava/lang/String;Ljava/lang/Boolean;Lorg/apache/logging/log4j/core/Filter;Ljava/lang/String;)Lio/sentry/log4j2/SentryAppender;
+ protected fun captureLog (Lorg/apache/logging/log4j/core/LogEvent;)V
+ public static fun createAppender (Ljava/lang/String;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Lorg/apache/logging/log4j/Level;Ljava/lang/String;Ljava/lang/Boolean;Lorg/apache/logging/log4j/core/Filter;Ljava/lang/String;)Lio/sentry/log4j2/SentryAppender;
protected fun createBreadcrumb (Lorg/apache/logging/log4j/core/LogEvent;)Lio/sentry/Breadcrumb;
protected fun createEvent (Lorg/apache/logging/log4j/core/LogEvent;)Lio/sentry/SentryEvent;
public fun start ()V
diff --git a/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java b/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java
index 1201344775..a32af15e2e 100644
--- a/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java
+++ b/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java
@@ -12,11 +12,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;
@@ -50,6 +54,7 @@ public class SentryAppender extends AbstractAppender {
private final @Nullable ITransportFactory transportFactory;
private @NotNull Level minimumBreadcrumbLevel = Level.INFO;
private @NotNull Level minimumEventLevel = Level.ERROR;
+ private @NotNull Level minimumLevel = Level.INFO;
private final @Nullable Boolean debug;
private final @NotNull IScopes scopes;
private final @Nullable List contextTags;
@@ -59,12 +64,42 @@ public class SentryAppender extends AbstractAppender {
.addPackage("maven:io.sentry:sentry-log4j2", BuildConfig.VERSION_NAME);
}
+ /**
+ * @deprecated This constructor is deprecated. Please use {@link #SentryAppender(String, Filter,
+ * String, Level, Level, Level, Boolean, ITransportFactory, IScopes, String[])} instead.
+ */
+ @Deprecated
+ @SuppressWarnings("InlineMeSuggester")
+ public SentryAppender(
+ final @NotNull String name,
+ final @Nullable Filter filter,
+ final @Nullable String dsn,
+ final @Nullable Level minimumBreadcrumbLevel,
+ final @Nullable Level minimumEventLevel,
+ final @Nullable Boolean debug,
+ final @Nullable ITransportFactory transportFactory,
+ final @NotNull IScopes scopes,
+ final @Nullable String[] contextTags) {
+ this(
+ name,
+ filter,
+ dsn,
+ minimumBreadcrumbLevel,
+ minimumEventLevel,
+ null,
+ debug,
+ transportFactory,
+ scopes,
+ contextTags);
+ }
+
public SentryAppender(
final @NotNull String name,
final @Nullable Filter filter,
final @Nullable String dsn,
final @Nullable Level minimumBreadcrumbLevel,
final @Nullable Level minimumEventLevel,
+ final @Nullable Level minimumLevel,
final @Nullable Boolean debug,
final @Nullable ITransportFactory transportFactory,
final @NotNull IScopes scopes,
@@ -77,6 +112,9 @@ public SentryAppender(
if (minimumEventLevel != null) {
this.minimumEventLevel = minimumEventLevel;
}
+ if (minimumLevel != null) {
+ this.minimumLevel = minimumLevel;
+ }
this.debug = debug;
this.transportFactory = transportFactory;
this.scopes = scopes;
@@ -89,6 +127,7 @@ public SentryAppender(
* @param name The name of the Appender.
* @param minimumBreadcrumbLevel The min. level of the breadcrumb.
* @param minimumEventLevel The min. level of the event.
+ * @param minimumLevel The min. level of the log event.
* @param dsn the Sentry DSN.
* @param debug if Sentry debug mode should be on
* @param filter The filter, if any, to use.
@@ -99,6 +138,7 @@ public SentryAppender(
@Nullable @PluginAttribute("name") final String name,
@Nullable @PluginAttribute("minimumBreadcrumbLevel") final Level minimumBreadcrumbLevel,
@Nullable @PluginAttribute("minimumEventLevel") final Level minimumEventLevel,
+ @Nullable @PluginAttribute("minimumLevel") final Level minimumLevel,
@Nullable @PluginAttribute("dsn") final String dsn,
@Nullable @PluginAttribute("debug") final Boolean debug,
@Nullable @PluginElement("filter") final Filter filter,
@@ -114,6 +154,7 @@ public SentryAppender(
dsn,
minimumBreadcrumbLevel,
minimumEventLevel,
+ minimumLevel,
debug,
null,
ScopesAdapter.getInstance(),
@@ -150,6 +191,9 @@ public void start() {
@Override
public void append(final @NotNull LogEvent eventObject) {
+ if (eventObject.getLevel().isMoreSpecificThan(minimumLevel)) {
+ captureLog(eventObject);
+ }
if (eventObject.getLevel().isMoreSpecificThan(minimumEventLevel)) {
final Hint hint = new Hint();
hint.set(SENTRY_SYNTHETIC_EXCEPTION, eventObject);
@@ -164,6 +208,29 @@ public void append(final @NotNull LogEvent eventObject) {
}
}
+ /**
+ * Captures a Sentry log from Log4j2's {@link LogEvent}.
+ *
+ * @param loggingEvent the log4j2 event
+ */
+ // for the Android compatibility we must use old Java Date class
+ @SuppressWarnings("JdkObsolete")
+ protected void captureLog(@NotNull LogEvent loggingEvent) {
+ final @NotNull SentryLogLevel sentryLevel = toSentryLogLevel(loggingEvent.getLevel());
+
+ final @Nullable Object[] arguments = loggingEvent.getMessage().getParameters();
+ final @NotNull SentryAttributes attributes = SentryAttributes.of();
+
+ attributes.add(
+ SentryAttribute.stringAttribute(
+ "sentry.message.template", loggingEvent.getMessage().getFormat()));
+
+ final @NotNull String formattedMessage = loggingEvent.getMessage().getFormattedMessage();
+ final @NotNull SentryLogParameters params = SentryLogParameters.create(attributes);
+
+ Sentry.logger().log(sentryLevel, params, formattedMessage, arguments);
+ }
+
/**
* Creates {@link SentryEvent} from Log4j2 {@link LogEvent}.
*
@@ -271,6 +338,28 @@ public void append(final @NotNull LogEvent eventObject) {
}
}
+ /**
+ * Transforms a {@link Level} into an {@link SentryLogLevel}.
+ *
+ * @param level original level as defined in log4j.
+ * @return log level used within sentry.
+ */
+ private static @NotNull SentryLogLevel toSentryLogLevel(final @NotNull Level level) {
+ if (level.isMoreSpecificThan(Level.FATAL)) {
+ return SentryLogLevel.FATAL;
+ } else if (level.isMoreSpecificThan(Level.ERROR)) {
+ return SentryLogLevel.ERROR;
+ } else if (level.isMoreSpecificThan(Level.WARN)) {
+ return SentryLogLevel.WARN;
+ } else if (level.isMoreSpecificThan(Level.INFO)) {
+ return SentryLogLevel.INFO;
+ } else if (level.isMoreSpecificThan(Level.DEBUG)) {
+ return SentryLogLevel.DEBUG;
+ } else {
+ return SentryLogLevel.TRACE;
+ }
+ }
+
private @NotNull SdkVersion createSdkVersion(final @NotNull SentryOptions sentryOptions) {
SdkVersion sdkVersion = sentryOptions.getSdkVersion();
diff --git a/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt b/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt
index 83f5ae2b22..25de2bd735 100644
--- a/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt
+++ b/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt
@@ -5,7 +5,9 @@ import io.sentry.InitPriority
import io.sentry.ScopesAdapter
import io.sentry.Sentry
import io.sentry.SentryLevel
+import io.sentry.SentryLogLevel
import io.sentry.checkEvent
+import io.sentry.checkLogs
import io.sentry.test.initForTest
import io.sentry.transport.ITransport
import java.time.Instant
@@ -49,6 +51,7 @@ class SentryAppenderTest {
transportFactory: ITransportFactory? = null,
minimumBreadcrumbLevel: Level? = null,
minimumEventLevel: Level? = null,
+ minimumLevel: Level? = null,
debug: Boolean? = null,
contextTags: List? = null,
): ExtendedLogger {
@@ -64,6 +67,7 @@ class SentryAppenderTest {
"http://key@localhost/proj",
minimumBreadcrumbLevel,
minimumEventLevel,
+ minimumLevel,
debug,
this.transportFactory,
ScopesAdapter.getInstance(),
@@ -239,6 +243,72 @@ class SentryAppenderTest {
.send(checkEvent { event -> assertEquals(SentryLevel.FATAL, event.level) }, anyOrNull())
}
+ @Test
+ fun `converts trace log level to Sentry log level`() {
+ val logger = fixture.getSut(minimumLevel = Level.TRACE)
+ logger.trace("testing trace level")
+
+ Sentry.flush(1000)
+
+ verify(fixture.transport)
+ .send(checkLogs { event -> assertEquals(SentryLogLevel.TRACE, event.items.first().level) })
+ }
+
+ @Test
+ fun `converts debug log level to Sentry log level`() {
+ val logger = fixture.getSut(minimumLevel = Level.DEBUG)
+ logger.debug("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`() {
+ val logger = fixture.getSut(minimumLevel = Level.INFO)
+ 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`() {
+ val logger = fixture.getSut(minimumLevel = Level.WARN)
+ logger.warn("testing warn level")
+
+ Sentry.flush(1000)
+
+ verify(fixture.transport)
+ .send(checkLogs { event -> assertEquals(SentryLogLevel.WARN, event.items.first().level) })
+ }
+
+ @Test
+ fun `converts error log level to Sentry log level`() {
+ val logger = fixture.getSut(minimumLevel = Level.ERROR)
+ logger.error("testing error level")
+
+ Sentry.flush(1000)
+
+ verify(fixture.transport)
+ .send(checkLogs { event -> assertEquals(SentryLogLevel.ERROR, event.items.first().level) })
+ }
+
+ @Test
+ fun `converts fatal log level to Sentry log level`() {
+ val logger = fixture.getSut(minimumLevel = Level.FATAL)
+ logger.fatal("testing fatal level")
+
+ Sentry.flush(1000)
+
+ verify(fixture.transport)
+ .send(checkLogs { event -> assertEquals(SentryLogLevel.FATAL, event.items.first().level) })
+ }
+
@Test
fun `attaches thread information`() {
val logger = fixture.getSut(minimumEventLevel = Level.WARN)
diff --git a/sentry-log4j2/src/test/resources/sentry.properties b/sentry-log4j2/src/test/resources/sentry.properties
index 12c5db4eb9..0163b4f2f8 100644
--- a/sentry-log4j2/src/test/resources/sentry.properties
+++ b/sentry-log4j2/src/test/resources/sentry.properties
@@ -1 +1,2 @@
release=release from sentry.properties
+logs.enabled=true
diff --git a/sentry-samples/sentry-samples-log4j2/build.gradle.kts b/sentry-samples/sentry-samples-log4j2/build.gradle.kts
index 6a1f648617..cf85847a57 100644
--- a/sentry-samples/sentry-samples-log4j2/build.gradle.kts
+++ b/sentry-samples/sentry-samples-log4j2/build.gradle.kts
@@ -14,4 +14,5 @@ configure {
dependencies {
implementation(projects.sentryLog4j2)
implementation(libs.log4j.api)
+ implementation(libs.log4j.core)
}
diff --git a/sentry-samples/sentry-samples-log4j2/src/main/resources/log4j2.xml b/sentry-samples/sentry-samples-log4j2/src/main/resources/log4j2.xml
index 6e8e6ffd48..51428b0f1c 100644
--- a/sentry-samples/sentry-samples-log4j2/src/main/resources/log4j2.xml
+++ b/sentry-samples/sentry-samples-log4j2/src/main/resources/log4j2.xml
@@ -11,6 +11,7 @@
dsn="https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563"
minimumBreadcrumbLevel="DEBUG"
minimumEventLevel="WARN"
+ minimumLevel="DEBUG"
debug="true"
contextTags="userId,requestId"
/>
diff --git a/sentry-samples/sentry-samples-log4j2/src/main/resources/sentry.properties b/sentry-samples/sentry-samples-log4j2/src/main/resources/sentry.properties
index 552d770ce9..47c00f6405 100644
--- a/sentry-samples/sentry-samples-log4j2/src/main/resources/sentry.properties
+++ b/sentry-samples/sentry-samples-log4j2/src/main/resources/sentry.properties
@@ -1 +1,2 @@
in-app-includes="io.sentry.samples"
+logs.enabled=true