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