diff --git a/CHANGELOG.md b/CHANGELOG.md index e6dd7a0301e..b113faacfef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Add an explicit Logs opt-in to the Log4j2 appender ([#5941](https://github.com/getsentry/sentry-java/pull/5941)) - Add an explicit Logs opt-in to the Logback appender ([#5940](https://github.com/getsentry/sentry-java/pull/5940)) - Make `ISpan.startChild` overloads with `SpanOptions` public ([#5927](https://github.com/getsentry/sentry-java/pull/5927)) diff --git a/sentry-log4j2/api/sentry-log4j2.api b/sentry-log4j2/api/sentry-log4j2.api index 2a5d4bf7895..4906f5f3ed4 100644 --- a/sentry-log4j2/api/sentry-log4j2.api +++ b/sentry-log4j2/api/sentry-log4j2.api @@ -7,8 +7,10 @@ public class io/sentry/log4j2/SentryAppender : org/apache/logging/log4j/core/app 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 (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;ZLjava/lang/Boolean;Lio/sentry/ITransportFactory;Lio/sentry/IScopes;[Ljava/lang/String;)V public fun append (Lorg/apache/logging/log4j/core/LogEvent;)V 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/Boolean;Ljava/lang/String;Ljava/lang/Boolean;Lorg/apache/logging/log4j/core/Filter;Ljava/lang/String;)Lio/sentry/log4j2/SentryAppender; 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; 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 0218b53518d..78cfcd97005 100644 --- a/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java +++ b/sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java @@ -55,6 +55,7 @@ public class SentryAppender extends AbstractAppender { private @NotNull Level minimumBreadcrumbLevel = Level.INFO; private @NotNull Level minimumEventLevel = Level.ERROR; private @NotNull Level minimumLevel = Level.INFO; + private final boolean enableLogs; private final @Nullable Boolean debug; private final @NotNull IScopes scopes; private final @Nullable List contextTags; @@ -104,6 +105,32 @@ public SentryAppender( final @Nullable ITransportFactory transportFactory, final @NotNull IScopes scopes, final @Nullable String[] contextTags) { + this( + name, + filter, + dsn, + minimumBreadcrumbLevel, + minimumEventLevel, + minimumLevel, + false, + 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 boolean enableLogs, + final @Nullable Boolean debug, + final @Nullable ITransportFactory transportFactory, + final @NotNull IScopes scopes, + final @Nullable String[] contextTags) { super(name, filter, null, true, null); this.dsn = dsn; if (minimumBreadcrumbLevel != null) { @@ -115,6 +142,7 @@ public SentryAppender( if (minimumLevel != null) { this.minimumLevel = minimumLevel; } + this.enableLogs = enableLogs; this.debug = debug; this.transportFactory = transportFactory; this.scopes = scopes; @@ -133,12 +161,34 @@ public SentryAppender( * @param filter The filter, if any, to use. * @return The SentryAppender. */ + public static @Nullable SentryAppender createAppender( + final @Nullable String name, + final @Nullable Level minimumBreadcrumbLevel, + final @Nullable Level minimumEventLevel, + final @Nullable Level minimumLevel, + final @Nullable String dsn, + final @Nullable Boolean debug, + final @Nullable Filter filter, + final @Nullable String contextTags) { + return createAppender( + name, + minimumBreadcrumbLevel, + minimumEventLevel, + minimumLevel, + false, + dsn, + debug, + filter, + contextTags); + } + @PluginFactory public static @Nullable SentryAppender createAppender( @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("enableLogs") final Boolean enableLogs, @Nullable @PluginAttribute("dsn") final String dsn, @Nullable @PluginAttribute("debug") final Boolean debug, @Nullable @PluginElement("filter") final Filter filter, @@ -155,6 +205,7 @@ public SentryAppender( minimumBreadcrumbLevel, minimumEventLevel, minimumLevel, + Boolean.TRUE.equals(enableLogs), debug, null, ScopesAdapter.getInstance(), @@ -206,7 +257,8 @@ void start(final @NotNull Sentry.OptionsConfiguration optionsConf @Override public void append(final @NotNull LogEvent eventObject) { - if (scopes.getOptions().getLogs().isEnabled() + if (enableLogs + && scopes.getOptions().getLogs().isEnabled() && eventObject.getLevel().isMoreSpecificThan(minimumLevel)) { captureLog(eventObject); } 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 c32459ea022..cb33115e647 100644 --- a/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt +++ b/sentry-log4j2/src/test/kotlin/io/sentry/log4j2/SentryAppenderTest.kt @@ -1,5 +1,6 @@ package io.sentry.log4j2 +import io.sentry.IScopes import io.sentry.ITransportFactory import io.sentry.InitPriority import io.sentry.ScopesAdapter @@ -28,6 +29,7 @@ import org.apache.logging.log4j.Level import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.MarkerManager import org.apache.logging.log4j.ThreadContext +import org.apache.logging.log4j.core.LogEvent import org.apache.logging.log4j.core.LoggerContext import org.apache.logging.log4j.core.config.AppenderRef import org.apache.logging.log4j.core.config.Configuration @@ -35,7 +37,10 @@ import org.apache.logging.log4j.core.config.LoggerConfig import org.apache.logging.log4j.spi.ExtendedLogger import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doNothing import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -57,6 +62,8 @@ class SentryAppenderTest { minimumLevel: Level? = null, debug: Boolean? = null, contextTags: List? = null, + enableLogs: Boolean = true, + enableGlobalLogs: Boolean = true, ): ExtendedLogger { if (transportFactory != null) { this.transportFactory = transportFactory @@ -71,6 +78,7 @@ class SentryAppenderTest { minimumBreadcrumbLevel, minimumEventLevel, minimumLevel, + enableLogs, debug, this.transportFactory, ScopesAdapter.getInstance(), @@ -98,6 +106,7 @@ class SentryAppenderTest { appender.start( appender.getOptionsConfiguration { options -> + options.logs.isEnabled = enableGlobalLogs options.logs.loggerBatchProcessorFactory = ILoggerBatchProcessorFactory { options, client -> LoggerBatchProcessor(options, client, ImmediateExecutorService()) @@ -253,6 +262,168 @@ class SentryAppenderTest { .send(checkEvent { event -> assertEquals(SentryLevel.FATAL, event.level) }, anyOrNull()) } + @Test + fun `does not capture logs when local logs are disabled`() { + val logger = fixture.getSut(enableLogs = false, enableGlobalLogs = true) + + logger.info("this should not be captured as a log") + Sentry.flush(10) + + verify(fixture.transport, never()).send(checkLogs {}) + } + + @Test + fun `captures logs when local and aggregate logs are enabled`() { + val logger = fixture.getSut(enableLogs = true, enableGlobalLogs = true) + + logger.info("this should be captured as a log") + Sentry.flush(10) + + verify(fixture.transport) + .send( + checkLogs { logs -> + assertEquals("this should be captured as a log", logs.items.first().body) + } + ) + } + + @Test + fun `captures events and breadcrumbs when local logs are disabled`() { + val logger = + fixture.getSut( + minimumBreadcrumbLevel = Level.INFO, + minimumEventLevel = Level.ERROR, + enableLogs = false, + enableGlobalLogs = true, + ) + + logger.info("this should be a breadcrumb") + logger.error("this should be an event") + Sentry.flush(10) + + verify(fixture.transport) + .send( + checkEvent { event -> + assertEquals("this should be an event", event.message?.formatted) + assertEquals("this should be a breadcrumb", event.breadcrumbs?.single()?.message) + }, + anyOrNull(), + ) + verify(fixture.transport, never()).send(checkLogs {}) + } + + @Test + fun `existing constructors default logs to disabled`() { + val scopes = mock() + val event = mock() + whenever(event.level).thenReturn(Level.INFO) + + val deprecatedAppender = + SentryAppender( + "deprecated", + null, + null, + Level.OFF, + Level.OFF, + null, + null, + scopes, + null, + ) + val existingAppender = + SentryAppender( + "existing", + null, + null, + Level.OFF, + Level.OFF, + Level.INFO, + null, + null, + scopes, + null, + ) + + deprecatedAppender.append(event) + existingAppender.append(event) + + verify(scopes, never()).logger() + } + + @Test + fun `existing factory and plugin attribute default logs to disabled`() { + val event = mock() + whenever(event.level).thenReturn(Level.INFO) + val existingAppender = + spy( + assertNotNull( + SentryAppender.createAppender( + "existing", + Level.OFF, + Level.OFF, + Level.INFO, + null, + null, + null, + null, + ) + ) + ) + val pluginDefaultAppender = + spy( + assertNotNull( + SentryAppender.createAppender( + "plugin-default", + Level.OFF, + Level.OFF, + Level.INFO, + null, + null, + null, + null, + null, + ) + ) + ) + + existingAppender.append(event) + pluginDefaultAppender.append(event) + + verify(existingAppender, never()).captureLog(event) + verify(pluginDefaultAppender, never()).captureLog(event) + } + + @Test + fun `plugin attribute enables logs with explicit opt in`() { + initForTest { + it.dsn = "http://key@localhost/proj" + it.logs.isEnabled = true + } + val event = mock() + whenever(event.level).thenReturn(Level.INFO) + val appender = + spy( + assertNotNull( + SentryAppender.createAppender( + "enabled", + Level.OFF, + Level.OFF, + Level.INFO, + true, + null, + null, + null, + null, + ) + ) + ) + doNothing().whenever(appender).captureLog(event) + + appender.append(event) + + verify(appender).captureLog(event) + } + @Test fun `converts trace log level to Sentry log level`() { val logger = fixture.getSut(minimumLevel = Level.TRACE) 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 51428b0f1cc..028e449b06a 100644 --- a/sentry-samples/sentry-samples-log4j2/src/main/resources/log4j2.xml +++ b/sentry-samples/sentry-samples-log4j2/src/main/resources/log4j2.xml @@ -12,6 +12,7 @@ minimumBreadcrumbLevel="DEBUG" minimumEventLevel="WARN" minimumLevel="DEBUG" + enableLogs="true" debug="true" contextTags="userId,requestId" />