From 70210c5c765a3a06c1ea78c53d10a770b4b4a8fc Mon Sep 17 00:00:00 2001 From: Daniel Mohedano Date: Thu, 1 Oct 2026 11:08:39 +0200 Subject: [PATCH 1/5] feat(logback): support agentless log submission --- .../logback/LogbackLoggerInstrumentation.java | 11 +- .../logback/LoggingEventInstrumentation.java | 2 +- .../logback/LogsIntakeHelper.java | 35 ++- .../groovy/LogbackLogsIntakeForkedTest.groovy | 228 ++++++++++++++++++ .../src/test/java/LogbackTestApplication.java | 117 +++++++++ 5 files changed, 389 insertions(+), 4 deletions(-) create mode 100644 dd-java-agent/instrumentation/logback-1.0/src/test/groovy/LogbackLogsIntakeForkedTest.groovy create mode 100644 dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestApplication.java diff --git a/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogbackLoggerInstrumentation.java b/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogbackLoggerInstrumentation.java index 51fb2870370..66a860b7875 100644 --- a/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogbackLoggerInstrumentation.java +++ b/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogbackLoggerInstrumentation.java @@ -9,8 +9,10 @@ import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.traceConfig; import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isBootstrapClassLoader; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.not; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; @@ -24,6 +26,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; import java.util.Map; import net.bytebuddy.asm.Advice; +import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumenterModule.class) public class LogbackLoggerInstrumentation extends InstrumenterModule.ContextTracking @@ -33,6 +36,11 @@ public LogbackLoggerInstrumentation() { super("logback", "logs-intake", "logs-intake-logback"); } + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return not(isBootstrapClassLoader()); + } + @Override public String instrumentedType() { return "ch.qos.logback.classic.Logger"; @@ -60,7 +68,8 @@ public void methodAdvice(MethodTransformer transformer) { LogbackLoggerInstrumentation.class.getName() + "$CallAppendersAdvice"); // this can be moved into a always on instrumenter module if one day context tracking can be // deactivated by config - if (InstrumenterConfig.get().isAppLogsCollectionEnabled()) { + if (InstrumenterConfig.get().isAppLogsCollectionEnabled() + || InstrumenterConfig.get().isAgentlessLogSubmissionEnabled()) { transformer.applyAdvice( isMethod() .and(isPublic()) diff --git a/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LoggingEventInstrumentation.java b/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LoggingEventInstrumentation.java index 2d78ccb8336..18ac0837924 100644 --- a/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LoggingEventInstrumentation.java +++ b/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LoggingEventInstrumentation.java @@ -27,7 +27,7 @@ import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumenterModule.class) -public class LoggingEventInstrumentation extends InstrumenterModule.Tracing +public class LoggingEventInstrumentation extends InstrumenterModule.ContextTracking implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice { public LoggingEventInstrumentation() { super("logback"); diff --git a/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogsIntakeHelper.java b/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogsIntakeHelper.java index 593acd82508..6aa961a6c36 100644 --- a/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogsIntakeHelper.java +++ b/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogsIntakeHelper.java @@ -1,7 +1,11 @@ package datadog.trace.instrumentation.logback; +import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.traceConfig; + +import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.StackTraceElementProxy; +import datadog.trace.api.Config; import datadog.trace.api.CorrelationIdentifier; import datadog.trace.api.logging.intake.LogsIntake; import java.util.Arrays; @@ -11,8 +15,16 @@ public class LogsIntakeHelper { + private static final boolean APP_LOGS_COLLECTION = Config.get().isAppLogsCollectionEnabled(); + private static final Level SUBMISSION_LEVEL = + Level.toLevel(Config.get().getAgentlessLogSubmissionLevel(), Level.INFO); + public static void log(ILoggingEvent event) { - LogsIntake.log(map(event)); + // App-log collection keeps its existing payload and framework-level filtering, even when + // both collection flags are enabled. + if (APP_LOGS_COLLECTION || event.getLevel().isGreaterOrEqual(SUBMISSION_LEVEL)) { + LogsIntake.log(map(event)); + } } private static Map map(ILoggingEvent event) { @@ -32,6 +44,26 @@ private static Map map(ILoggingEvent event) { thrownLog.put("extendedStackTrace", stackTraceString); log.put("thrown", thrownLog); } + if (!APP_LOGS_COLLECTION) { + Map context = new HashMap<>(); + // Capture on the producer thread, independently of the ordering of the two entry advices. + // Injection-disabled logs retain user MDC but do not gain automatic correlation IDs. + if (traceConfig().isLogsInjectionEnabled()) { + addCorrelationIds(context); + } + Map mdc = event.getMDCPropertyMap(); + if (mdc != null) { + context.putAll(mdc); + } + log.put("contextMap", context); + log.put("timestamp", event.getTimeStamp()); + } else { + addCorrelationIds(log); + } + return log; + } + + private static void addCorrelationIds(Map log) { String traceId = CorrelationIdentifier.getTraceId(); if (traceId != null && !traceId.equals("0")) { log.put("dd.trace_id", traceId); @@ -40,6 +72,5 @@ private static Map map(ILoggingEvent event) { if (spanId != null && !spanId.equals("0")) { log.put("dd.span_id", spanId); } - return log; } } diff --git a/dd-java-agent/instrumentation/logback-1.0/src/test/groovy/LogbackLogsIntakeForkedTest.groovy b/dd-java-agent/instrumentation/logback-1.0/src/test/groovy/LogbackLogsIntakeForkedTest.groovy new file mode 100644 index 00000000000..f41ab786cd3 --- /dev/null +++ b/dd-java-agent/instrumentation/logback-1.0/src/test/groovy/LogbackLogsIntakeForkedTest.groovy @@ -0,0 +1,228 @@ +import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.api.CorrelationIdentifier +import datadog.trace.api.logging.intake.LogsIntake +import datadog.trace.api.logging.intake.LogsWriter +import datadog.trace.bootstrap.instrumentation.api.AgentTracer +import spock.lang.Shared + +import java.util.concurrent.CopyOnWriteArrayList + +abstract class LogbackLogsIntakeTestBase extends InstrumentationSpecification { + static final List> MESSAGES = new CopyOnWriteArrayList<>() + @Shared + def app + + boolean agentless() { + true + } + boolean appLogs() { + false + } + boolean injection() { + true + } + boolean integration() { + true + } + + @Override + void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("agentless.log.submission.enabled", agentless().toString()) + injectSysConfig("app.logs.collection.enabled", appLogs().toString()) + injectSysConfig("logs.injection.enabled", injection().toString()) + injectSysConfig("integration.logback.enabled", integration().toString()) + injectSysConfig("agentless.log.submission.level", "WARN") + } + + def setupSpec() { + app = new LogbackTestClassLoader(getClass().classLoader).loadClass("LogbackTestApplication").getDeclaredConstructor().newInstance() + LogsIntake.registerWriter(new LogsWriter() { + void log(Map message) { + if (message.loggerName == "test.logback.intake") { + MESSAGES.add(message) + } + } + void start() {} + void shutdown() {} + }) + } + + def cleanupSpec() { + LogsIntake.registerWriter(null) + } + + def setup() { + MESSAGES.clear() + app.clear() + app.contexts.clear() + } + + def cleanup() { + app.clear() + } + + def "flags preserve opt-in, app-log precedence and framework filtering"() { + when: + app.log("DEBUG", "debug") + app.log("INFO", "info") + app.log("WARN", "formatted message") + app.log("ERROR", "error") + + then: + MESSAGES*.message == (!integration() || (!agentless() && !appLogs()) ? [] : + appLogs() ? ["debug", "info", "formatted message", "error"] : ["formatted message", "error"]) + if (!MESSAGES.empty) { + assert MESSAGES.last().thrown.name == IllegalStateException.name + assert MESSAGES.last().thrown.message == "failure" + assert MESSAGES.last().thrown.extendedStackTrace + assert MESSAGES.every { it.containsKey("contextMap") == !appLogs() } + assert MESSAGES.every { it.containsKey("timestamp") == !appLogs() } + } + } + + def "producer correlation honors injection switch and snapshots MDC before wrapper cleanup"() { + setup: + def span = AgentTracer.startSpan("test", "logback-producer") + def scope = AgentTracer.activateSpan(span) + def traceId = CorrelationIdentifier.traceId + def spanId = CorrelationIdentifier.spanId + app.put("custom", "before-clear") + + when: + app.log("WARN", "active") + app.put("dd.trace_id", "user-trace") + app.log("WARN", "collision") + app.clear() + scope.close() + span.finish() + app.log("WARN", "outside") + + then: + app.contexts.size() == 3 + app.contexts[0]["dd.trace_id"] == (integration() && injection() ? traceId : null) + app.contexts[0]["dd.span_id"] == (integration() && injection() ? spanId : null) + app.contexts[1]["dd.trace_id"] == "user-trace" + !app.contexts[2].containsKey("dd.trace_id") + if (integration() && (agentless() || appLogs())) { + assert MESSAGES.size() == 3 + def first = appLogs() ? MESSAGES[0] : MESSAGES[0].contextMap + assert first["dd.trace_id"] == (appLogs() || injection() ? traceId : null) + assert first["dd.span_id"] == (appLogs() || injection() ? spanId : null) + if (!appLogs()) { + assert first.custom == "before-clear" + assert MESSAGES[1].contextMap["dd.trace_id"] == "user-trace" + assert !MESSAGES[2].contextMap.containsKey("dd.trace_id") + } else { + assert MESSAGES[1]["dd.trace_id"] == traceId + assert !MESSAGES[2].containsKey("dd.trace_id") + } + } else { + assert MESSAGES.empty + } + } +} + +class LogbackLogsIntakeForkedTest extends LogbackLogsIntakeTestBase { + def "async delivery after scope closes retains producer IDs exactly once"() { + setup: + app.startAsync() + def span = AgentTracer.startSpan("test", "async-producer") + def scope = AgentTracer.activateSpan(span) + def traceId = CorrelationIdentifier.traceId + def spanId = CorrelationIdentifier.spanId + + when: + app.log("WARN", "async") + scope.close() + span.finish() + def deliveredContext = app.finishAsync() + + then: + MESSAGES.size() == 1 + MESSAGES[0].contextMap["dd.trace_id"] == traceId + MESSAGES[0].contextMap["dd.span_id"] == spanId + deliveredContext["dd.span_id"] == spanId + } + + def "JUL bridge reaches the same backend hook once"() { + when: + app.jul() + + then: + MESSAGES*.message == ["bridged"] + } +} + +class LogbackNoSubmissionForkedTest extends LogbackLogsIntakeTestBase { + boolean agentless() { + false + } +} + +class LogbackAppLogsForkedTest extends LogbackLogsIntakeTestBase { + boolean agentless() { + false + } + boolean appLogs() { + true + } +} + +class LogbackBothFlagsForkedTest extends LogbackLogsIntakeTestBase { + boolean appLogs() { + true + } +} + +class LogbackNoInjectionForkedTest extends LogbackLogsIntakeTestBase { + boolean injection() { + false + } +} + +class LogbackDisabledForkedTest extends LogbackLogsIntakeTestBase { + boolean integration() { + false + } +} + +class LogbackCiAgentlessForkedTest extends LogbackLogsIntakeTestBase { + @Override + void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("trace.enabled", "false") + } +} + +class LogbackCiCorrelationForkedTest extends LogbackNoSubmissionForkedTest { + @Override + void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("trace.enabled", "false") + } +} + +/** The harness boot-loads its logger; exercise a separate application-loaded backend. */ +@groovy.transform.CompileStatic +class LogbackTestClassLoader extends ClassLoader { + LogbackTestClassLoader(ClassLoader parent) { + super(parent) + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) { + if (!name.startsWith("ch.qos.logback.") && !name.startsWith("org.slf4j.") && !name.startsWith("LogbackTestApplication")) { + return super.loadClass(name, resolve) + } + def loaded = findLoadedClass(name) + if (loaded == null) { + byte[] bytes = getResourceAsStream(name.replace('.', '/') + '.class').withCloseable { it.bytes } + loaded = defineClass(name, bytes, 0, bytes.length) + } + if (resolve) { + resolveClass(loaded) + } + return loaded + } +} diff --git a/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestApplication.java b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestApplication.java new file mode 100644 index 00000000000..a3231923413 --- /dev/null +++ b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestApplication.java @@ -0,0 +1,117 @@ +import ch.qos.logback.classic.AsyncAppender; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; +import org.slf4j.bridge.SLF4JBridgeHandler; + +public class LogbackTestApplication { + private final Logger logger = (Logger) LoggerFactory.getLogger("test.logback.intake"); + private final CountDownLatch release = new CountDownLatch(1); + private final CountDownLatch delivered = new CountDownLatch(1); + private Map deliveredContext; + private AsyncAppender async; + private final List> contexts = new ArrayList<>(); + + public LogbackTestApplication() { + logger.setLevel(Level.DEBUG); + AppenderBase sink = + new AppenderBase() { + @Override + protected void append(ILoggingEvent event) { + contexts.add(new HashMap<>(event.getMDCPropertyMap())); + } + }; + sink.setContext(logger.getLoggerContext()); + sink.start(); + logger.addAppender(sink); + } + + public List> getContexts() { + return contexts; + } + + public void log(String level, String message) { + switch (level) { + case "DEBUG": + logger.debug(message); + break; + case "INFO": + logger.info(message); + break; + case "ERROR": + logger.error(message, new IllegalStateException("failure")); + break; + default: + logger.warn("{}", message); + } + } + + public void put(String key, String value) { + MDC.put(key, value); + } + + public void clear() { + MDC.clear(); + } + + public void startAsync() { + AppenderBase sink = + new AppenderBase() { + @Override + protected void append(ILoggingEvent event) { + try { + if (release.await(5, TimeUnit.SECONDS)) { + deliveredContext = new HashMap<>(event.getMDCPropertyMap()); + delivered.countDown(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }; + sink.setContext(logger.getLoggerContext()); + sink.start(); + async = new AsyncAppender(); + async.setContext(logger.getLoggerContext()); + async.addAppender(sink); + async.start(); + logger.addAppender(async); + } + + public Map finishAsync() throws InterruptedException { + release.countDown(); + try { + if (!delivered.await(5, TimeUnit.SECONDS)) { + throw new AssertionError("AsyncAppender did not deliver"); + } + return deliveredContext; + } finally { + logger.detachAppender(async); + async.stop(); + } + } + + public void jul() { + java.util.logging.Logger jul = java.util.logging.Logger.getLogger(logger.getName()); + SLF4JBridgeHandler bridge = new SLF4JBridgeHandler(); + boolean parentHandlers = jul.getUseParentHandlers(); + jul.setUseParentHandlers(false); + jul.addHandler(bridge); + try { + jul.warning("bridged"); + } finally { + jul.removeHandler(bridge); + jul.setUseParentHandlers(parentHandlers); + bridge.close(); + } + } +} From 0028e118621e8870f2b957ad30d46c86f11bd63f Mon Sep 17 00:00:00 2001 From: Daniel Mohedano Date: Thu, 1 Oct 2026 11:10:59 +0200 Subject: [PATCH 2/5] test(logback): cover agentless submission in log injection smoke tests --- .../test/groovy/datadog/smoketest/LogInjectionSmokeTest.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dd-smoke-tests/log-injection/src/test/groovy/datadog/smoketest/LogInjectionSmokeTest.groovy b/dd-smoke-tests/log-injection/src/test/groovy/datadog/smoketest/LogInjectionSmokeTest.groovy index f409301b9b2..bb874dee707 100644 --- a/dd-smoke-tests/log-injection/src/test/groovy/datadog/smoketest/LogInjectionSmokeTest.groovy +++ b/dd-smoke-tests/log-injection/src/test/groovy/datadog/smoketest/LogInjectionSmokeTest.groovy @@ -31,6 +31,7 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { static final int TIMEOUT_SECS = 30 static final String LOG4J2_BACKEND = "Log4j2" + static final Set DIRECT_LOG_SUBMISSION_BACKENDS = [LOG4J2_BACKEND, "Logback"] as Set @Shared File outputLogFile @@ -185,7 +186,7 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { } def supportsDirectLogSubmission() { - return backend() == LOG4J2_BACKEND + return backend() in DIRECT_LOG_SUBMISSION_BACKENDS } def supportsAppLogCollection() { From 75926178b727bffa4a86a29739cd5479087c6a9c Mon Sep 17 00:00:00 2001 From: Daniel Mohedano Date: Fri, 2 Oct 2026 11:31:36 +0200 Subject: [PATCH 3/5] test(logback): migrate agentless log submission tests to JUnit 5 --- .../groovy/LogbackLogsIntakeForkedTest.groovy | 228 ----------------- .../src/test/java/LogbackApplication.java | 19 ++ .../java/LogbackLogsIntakeForkedTest.java | 241 ++++++++++++++++++ .../src/test/java/LogbackTestApplication.java | 9 +- .../src/test/java/LogbackTestClassLoader.java | 45 ++++ 5 files changed, 313 insertions(+), 229 deletions(-) delete mode 100644 dd-java-agent/instrumentation/logback-1.0/src/test/groovy/LogbackLogsIntakeForkedTest.groovy create mode 100644 dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackApplication.java create mode 100644 dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackLogsIntakeForkedTest.java create mode 100644 dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestClassLoader.java diff --git a/dd-java-agent/instrumentation/logback-1.0/src/test/groovy/LogbackLogsIntakeForkedTest.groovy b/dd-java-agent/instrumentation/logback-1.0/src/test/groovy/LogbackLogsIntakeForkedTest.groovy deleted file mode 100644 index f41ab786cd3..00000000000 --- a/dd-java-agent/instrumentation/logback-1.0/src/test/groovy/LogbackLogsIntakeForkedTest.groovy +++ /dev/null @@ -1,228 +0,0 @@ -import datadog.trace.agent.test.InstrumentationSpecification -import datadog.trace.api.CorrelationIdentifier -import datadog.trace.api.logging.intake.LogsIntake -import datadog.trace.api.logging.intake.LogsWriter -import datadog.trace.bootstrap.instrumentation.api.AgentTracer -import spock.lang.Shared - -import java.util.concurrent.CopyOnWriteArrayList - -abstract class LogbackLogsIntakeTestBase extends InstrumentationSpecification { - static final List> MESSAGES = new CopyOnWriteArrayList<>() - @Shared - def app - - boolean agentless() { - true - } - boolean appLogs() { - false - } - boolean injection() { - true - } - boolean integration() { - true - } - - @Override - void configurePreAgent() { - super.configurePreAgent() - injectSysConfig("agentless.log.submission.enabled", agentless().toString()) - injectSysConfig("app.logs.collection.enabled", appLogs().toString()) - injectSysConfig("logs.injection.enabled", injection().toString()) - injectSysConfig("integration.logback.enabled", integration().toString()) - injectSysConfig("agentless.log.submission.level", "WARN") - } - - def setupSpec() { - app = new LogbackTestClassLoader(getClass().classLoader).loadClass("LogbackTestApplication").getDeclaredConstructor().newInstance() - LogsIntake.registerWriter(new LogsWriter() { - void log(Map message) { - if (message.loggerName == "test.logback.intake") { - MESSAGES.add(message) - } - } - void start() {} - void shutdown() {} - }) - } - - def cleanupSpec() { - LogsIntake.registerWriter(null) - } - - def setup() { - MESSAGES.clear() - app.clear() - app.contexts.clear() - } - - def cleanup() { - app.clear() - } - - def "flags preserve opt-in, app-log precedence and framework filtering"() { - when: - app.log("DEBUG", "debug") - app.log("INFO", "info") - app.log("WARN", "formatted message") - app.log("ERROR", "error") - - then: - MESSAGES*.message == (!integration() || (!agentless() && !appLogs()) ? [] : - appLogs() ? ["debug", "info", "formatted message", "error"] : ["formatted message", "error"]) - if (!MESSAGES.empty) { - assert MESSAGES.last().thrown.name == IllegalStateException.name - assert MESSAGES.last().thrown.message == "failure" - assert MESSAGES.last().thrown.extendedStackTrace - assert MESSAGES.every { it.containsKey("contextMap") == !appLogs() } - assert MESSAGES.every { it.containsKey("timestamp") == !appLogs() } - } - } - - def "producer correlation honors injection switch and snapshots MDC before wrapper cleanup"() { - setup: - def span = AgentTracer.startSpan("test", "logback-producer") - def scope = AgentTracer.activateSpan(span) - def traceId = CorrelationIdentifier.traceId - def spanId = CorrelationIdentifier.spanId - app.put("custom", "before-clear") - - when: - app.log("WARN", "active") - app.put("dd.trace_id", "user-trace") - app.log("WARN", "collision") - app.clear() - scope.close() - span.finish() - app.log("WARN", "outside") - - then: - app.contexts.size() == 3 - app.contexts[0]["dd.trace_id"] == (integration() && injection() ? traceId : null) - app.contexts[0]["dd.span_id"] == (integration() && injection() ? spanId : null) - app.contexts[1]["dd.trace_id"] == "user-trace" - !app.contexts[2].containsKey("dd.trace_id") - if (integration() && (agentless() || appLogs())) { - assert MESSAGES.size() == 3 - def first = appLogs() ? MESSAGES[0] : MESSAGES[0].contextMap - assert first["dd.trace_id"] == (appLogs() || injection() ? traceId : null) - assert first["dd.span_id"] == (appLogs() || injection() ? spanId : null) - if (!appLogs()) { - assert first.custom == "before-clear" - assert MESSAGES[1].contextMap["dd.trace_id"] == "user-trace" - assert !MESSAGES[2].contextMap.containsKey("dd.trace_id") - } else { - assert MESSAGES[1]["dd.trace_id"] == traceId - assert !MESSAGES[2].containsKey("dd.trace_id") - } - } else { - assert MESSAGES.empty - } - } -} - -class LogbackLogsIntakeForkedTest extends LogbackLogsIntakeTestBase { - def "async delivery after scope closes retains producer IDs exactly once"() { - setup: - app.startAsync() - def span = AgentTracer.startSpan("test", "async-producer") - def scope = AgentTracer.activateSpan(span) - def traceId = CorrelationIdentifier.traceId - def spanId = CorrelationIdentifier.spanId - - when: - app.log("WARN", "async") - scope.close() - span.finish() - def deliveredContext = app.finishAsync() - - then: - MESSAGES.size() == 1 - MESSAGES[0].contextMap["dd.trace_id"] == traceId - MESSAGES[0].contextMap["dd.span_id"] == spanId - deliveredContext["dd.span_id"] == spanId - } - - def "JUL bridge reaches the same backend hook once"() { - when: - app.jul() - - then: - MESSAGES*.message == ["bridged"] - } -} - -class LogbackNoSubmissionForkedTest extends LogbackLogsIntakeTestBase { - boolean agentless() { - false - } -} - -class LogbackAppLogsForkedTest extends LogbackLogsIntakeTestBase { - boolean agentless() { - false - } - boolean appLogs() { - true - } -} - -class LogbackBothFlagsForkedTest extends LogbackLogsIntakeTestBase { - boolean appLogs() { - true - } -} - -class LogbackNoInjectionForkedTest extends LogbackLogsIntakeTestBase { - boolean injection() { - false - } -} - -class LogbackDisabledForkedTest extends LogbackLogsIntakeTestBase { - boolean integration() { - false - } -} - -class LogbackCiAgentlessForkedTest extends LogbackLogsIntakeTestBase { - @Override - void configurePreAgent() { - super.configurePreAgent() - injectSysConfig("trace.enabled", "false") - } -} - -class LogbackCiCorrelationForkedTest extends LogbackNoSubmissionForkedTest { - @Override - void configurePreAgent() { - super.configurePreAgent() - injectSysConfig("trace.enabled", "false") - } -} - -/** The harness boot-loads its logger; exercise a separate application-loaded backend. */ -@groovy.transform.CompileStatic -class LogbackTestClassLoader extends ClassLoader { - LogbackTestClassLoader(ClassLoader parent) { - super(parent) - } - - @Override - protected synchronized Class loadClass(String name, boolean resolve) { - if (!name.startsWith("ch.qos.logback.") && !name.startsWith("org.slf4j.") && !name.startsWith("LogbackTestApplication")) { - return super.loadClass(name, resolve) - } - def loaded = findLoadedClass(name) - if (loaded == null) { - byte[] bytes = getResourceAsStream(name.replace('.', '/') + '.class').withCloseable { it.bytes } - loaded = defineClass(name, bytes, 0, bytes.length) - } - if (resolve) { - resolveClass(loaded) - } - return loaded - } -} diff --git a/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackApplication.java b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackApplication.java new file mode 100644 index 00000000000..70bfbb405e4 --- /dev/null +++ b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackApplication.java @@ -0,0 +1,19 @@ +import java.util.List; +import java.util.Map; + +/** Logback test application loaded apart from the logging backend of the test harness. */ +public interface LogbackApplication { + List> getContexts(); + + void log(String level, String message); + + void put(String key, String value); + + void clear(); + + void startAsync(); + + Map finishAsync() throws InterruptedException; + + void jul(); +} diff --git a/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackLogsIntakeForkedTest.java b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackLogsIntakeForkedTest.java new file mode 100644 index 00000000000..2ab02261479 --- /dev/null +++ b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackLogsIntakeForkedTest.java @@ -0,0 +1,241 @@ +import static datadog.trace.api.config.GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED; +import static datadog.trace.api.config.GeneralConfig.AGENTLESS_LOG_SUBMISSION_LEVEL; +import static datadog.trace.api.config.GeneralConfig.APP_LOGS_COLLECTION_ENABLED; +import static datadog.trace.api.config.TraceInstrumentationConfig.LOGS_INJECTION_ENABLED; +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_ENABLED; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.context.ContextScope; +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.api.Config; +import datadog.trace.api.CorrelationIdentifier; +import datadog.trace.api.InstrumenterConfig; +import datadog.trace.api.logging.intake.LogsIntake; +import datadog.trace.api.logging.intake.LogsWriter; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.test.junit.utils.config.WithConfig; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Each subclass runs in its own JVM with the agent installed for its configuration. */ +@WithConfig(key = AGENTLESS_LOG_SUBMISSION_ENABLED, value = "true") +@WithConfig(key = APP_LOGS_COLLECTION_ENABLED, value = "false") +@WithConfig(key = LOGS_INJECTION_ENABLED, value = "true") +@WithConfig(key = "integration.logback.enabled", value = "true") +@WithConfig(key = AGENTLESS_LOG_SUBMISSION_LEVEL, value = "WARN") +abstract class AbstractLogbackLogsIntakeForkedTest extends AbstractInstrumentationTest { + static final String LOGGER_NAME = "test.logback.intake"; + static final List> MESSAGES = new CopyOnWriteArrayList<>(); + + static LogbackApplication app; + + @BeforeAll + static void setupApplication() throws Exception { + app = + (LogbackApplication) + new LogbackTestClassLoader(AbstractLogbackLogsIntakeForkedTest.class.getClassLoader()) + .loadClass("LogbackTestApplication") + .getDeclaredConstructor() + .newInstance(); + LogsIntake.registerWriter(new CapturingLogsWriter()); + } + + @AfterAll + static void unregisterWriter() { + LogsIntake.registerWriter(null); + } + + @BeforeEach + void clearState() { + MESSAGES.clear(); + app.clear(); + app.getContexts().clear(); + } + + @AfterEach + void clearMdc() { + app.clear(); + } + + static boolean agentless() { + return InstrumenterConfig.get().isAgentlessLogSubmissionEnabled(); + } + + static boolean appLogs() { + return InstrumenterConfig.get().isAppLogsCollectionEnabled(); + } + + static boolean injection() { + return Config.get().isLogsInjectionEnabled(); + } + + static boolean integration() { + return InstrumenterConfig.get().isIntegrationEnabled(singletonList("logback"), true); + } + + static boolean collected() { + return integration() && (agentless() || appLogs()); + } + + @Test + void flagsPreserveOptInAppLogPrecedenceAndFrameworkFiltering() { + app.log("DEBUG", "debug"); + app.log("INFO", "info"); + app.log("WARN", "formatted message"); + app.log("ERROR", "error"); + + List expected; + if (!collected()) { + expected = emptyList(); + } else if (appLogs()) { + expected = asList("debug", "info", "formatted message", "error"); + } else { + expected = asList("formatted message", "error"); + } + assertEquals(expected, messages()); + if (!MESSAGES.isEmpty()) { + Map thrown = (Map) MESSAGES.get(MESSAGES.size() - 1).get("thrown"); + assertEquals(IllegalStateException.class.getName(), thrown.get("name")); + assertEquals("failure", thrown.get("message")); + assertFalse(thrown.get("extendedStackTrace").toString().isEmpty()); + for (Map message : MESSAGES) { + assertEquals(!appLogs(), message.containsKey("contextMap")); + assertEquals(!appLogs(), message.containsKey("timestamp")); + } + } + } + + @Test + void producerCorrelationHonorsInjectionSwitchAndSnapshotsMdcBeforeWrapperCleanup() { + AgentSpan span = AgentTracer.startSpan("test", "logback-producer"); + String traceId; + String spanId; + try (ContextScope scope = AgentTracer.activateSpan(span)) { + traceId = CorrelationIdentifier.getTraceId(); + spanId = CorrelationIdentifier.getSpanId(); + app.put("custom", "before-clear"); + app.log("WARN", "active"); + app.put("dd.trace_id", "user-trace"); + app.log("WARN", "collision"); + app.clear(); + } finally { + span.finish(); + } + app.log("WARN", "outside"); + + List> contexts = app.getContexts(); + assertEquals(3, contexts.size()); + boolean injected = integration() && injection(); + assertEquals(injected ? traceId : null, contexts.get(0).get("dd.trace_id")); + assertEquals(injected ? spanId : null, contexts.get(0).get("dd.span_id")); + assertEquals("user-trace", contexts.get(1).get("dd.trace_id")); + assertFalse(contexts.get(2).containsKey("dd.trace_id")); + if (!collected()) { + assertTrue(MESSAGES.isEmpty()); + return; + } + assertEquals(3, MESSAGES.size()); + if (appLogs()) { + assertEquals(traceId, MESSAGES.get(0).get("dd.trace_id")); + assertEquals(spanId, MESSAGES.get(0).get("dd.span_id")); + assertEquals(traceId, MESSAGES.get(1).get("dd.trace_id")); + assertFalse(MESSAGES.get(2).containsKey("dd.trace_id")); + } else { + Map first = contextMap(0); + assertEquals(injection() ? traceId : null, first.get("dd.trace_id")); + assertEquals(injection() ? spanId : null, first.get("dd.span_id")); + assertEquals("before-clear", first.get("custom")); + assertEquals("user-trace", contextMap(1).get("dd.trace_id")); + assertFalse(contextMap(2).containsKey("dd.trace_id")); + } + } + + static List messages() { + return MESSAGES.stream().map(m -> (String) m.get("message")).collect(Collectors.toList()); + } + + static Map contextMap(int index) { + return (Map) MESSAGES.get(index).get("contextMap"); + } + + static class CapturingLogsWriter implements LogsWriter { + @Override + public void log(Map message) { + if (LOGGER_NAME.equals(message.get("loggerName"))) { + MESSAGES.add(message); + } + } + + @Override + public void start() {} + + @Override + public void shutdown() {} + } +} + +class LogbackLogsIntakeForkedTest extends AbstractLogbackLogsIntakeForkedTest { + @Test + void asyncDeliveryAfterScopeClosesRetainsProducerIdsExactlyOnce() throws Exception { + app.startAsync(); + AgentSpan span = AgentTracer.startSpan("test", "async-producer"); + String traceId; + String spanId; + try (ContextScope scope = AgentTracer.activateSpan(span)) { + traceId = CorrelationIdentifier.getTraceId(); + spanId = CorrelationIdentifier.getSpanId(); + app.log("WARN", "async"); + } finally { + span.finish(); + } + Map deliveredContext = app.finishAsync(); + + assertEquals(1, MESSAGES.size()); + assertEquals(traceId, contextMap(0).get("dd.trace_id")); + assertEquals(spanId, contextMap(0).get("dd.span_id")); + assertEquals(spanId, deliveredContext.get("dd.span_id")); + } + + @Test + void julBridgeReachesTheSameBackendHookOnce() { + app.jul(); + + assertEquals(singletonList("bridged"), messages()); + } +} + +@WithConfig(key = AGENTLESS_LOG_SUBMISSION_ENABLED, value = "false") +class LogbackNoSubmissionForkedTest extends AbstractLogbackLogsIntakeForkedTest {} + +@WithConfig(key = AGENTLESS_LOG_SUBMISSION_ENABLED, value = "false") +@WithConfig(key = APP_LOGS_COLLECTION_ENABLED, value = "true") +class LogbackAppLogsForkedTest extends AbstractLogbackLogsIntakeForkedTest {} + +@WithConfig(key = APP_LOGS_COLLECTION_ENABLED, value = "true") +class LogbackBothFlagsForkedTest extends AbstractLogbackLogsIntakeForkedTest {} + +@WithConfig(key = LOGS_INJECTION_ENABLED, value = "false") +class LogbackNoInjectionForkedTest extends AbstractLogbackLogsIntakeForkedTest {} + +@WithConfig(key = "integration.logback.enabled", value = "false") +class LogbackDisabledForkedTest extends AbstractLogbackLogsIntakeForkedTest {} + +@WithConfig(key = TRACE_ENABLED, value = "false") +class LogbackCiAgentlessForkedTest extends AbstractLogbackLogsIntakeForkedTest {} + +@WithConfig(key = AGENTLESS_LOG_SUBMISSION_ENABLED, value = "false") +@WithConfig(key = TRACE_ENABLED, value = "false") +class LogbackCiCorrelationForkedTest extends AbstractLogbackLogsIntakeForkedTest {} diff --git a/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestApplication.java b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestApplication.java index a3231923413..886f578603e 100644 --- a/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestApplication.java +++ b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestApplication.java @@ -13,7 +13,7 @@ import org.slf4j.MDC; import org.slf4j.bridge.SLF4JBridgeHandler; -public class LogbackTestApplication { +public class LogbackTestApplication implements LogbackApplication { private final Logger logger = (Logger) LoggerFactory.getLogger("test.logback.intake"); private final CountDownLatch release = new CountDownLatch(1); private final CountDownLatch delivered = new CountDownLatch(1); @@ -35,10 +35,12 @@ protected void append(ILoggingEvent event) { logger.addAppender(sink); } + @Override public List> getContexts() { return contexts; } + @Override public void log(String level, String message) { switch (level) { case "DEBUG": @@ -55,14 +57,17 @@ public void log(String level, String message) { } } + @Override public void put(String key, String value) { MDC.put(key, value); } + @Override public void clear() { MDC.clear(); } + @Override public void startAsync() { AppenderBase sink = new AppenderBase() { @@ -87,6 +92,7 @@ protected void append(ILoggingEvent event) { logger.addAppender(async); } + @Override public Map finishAsync() throws InterruptedException { release.countDown(); try { @@ -100,6 +106,7 @@ public Map finishAsync() throws InterruptedException { } } + @Override public void jul() { java.util.logging.Logger jul = java.util.logging.Logger.getLogger(logger.getName()); SLF4JBridgeHandler bridge = new SLF4JBridgeHandler(); diff --git a/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestClassLoader.java b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestClassLoader.java new file mode 100644 index 00000000000..ff59bcb761d --- /dev/null +++ b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackTestClassLoader.java @@ -0,0 +1,45 @@ +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** The harness boot-loads its logger; loads a separate application Logback and SLF4J. */ +public class LogbackTestClassLoader extends ClassLoader { + public LogbackTestClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (!name.startsWith("ch.qos.logback.") + && !name.startsWith("org.slf4j.") + && !name.startsWith("LogbackTestApplication")) { + return super.loadClass(name, resolve); + } + Class loaded = findLoadedClass(name); + if (loaded == null) { + byte[] bytes = readClass(name); + loaded = defineClass(name, bytes, 0, bytes.length); + } + if (resolve) { + resolveClass(loaded); + } + return loaded; + } + + private byte[] readClass(String name) throws ClassNotFoundException { + try (InputStream input = getResourceAsStream(name.replace('.', '/') + ".class")) { + if (input == null) { + throw new ClassNotFoundException(name); + } + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + for (int read; (read = input.read(buffer)) != -1; ) { + output.write(buffer, 0, read); + } + return output.toByteArray(); + } catch (IOException e) { + throw new ClassNotFoundException(name, e); + } + } +} From e33b73190939ce0183691f8efbcb2c5da5bbfe6c Mon Sep 17 00:00:00 2001 From: Daniel Mohedano Date: Fri, 2 Oct 2026 11:31:37 +0200 Subject: [PATCH 4/5] test(logback): use a constant for the Logback smoke test backend --- .../smoketest/LogInjectionSmokeTest.groovy | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/dd-smoke-tests/log-injection/src/test/groovy/datadog/smoketest/LogInjectionSmokeTest.groovy b/dd-smoke-tests/log-injection/src/test/groovy/datadog/smoketest/LogInjectionSmokeTest.groovy index bb874dee707..f6a65371ec5 100644 --- a/dd-smoke-tests/log-injection/src/test/groovy/datadog/smoketest/LogInjectionSmokeTest.groovy +++ b/dd-smoke-tests/log-injection/src/test/groovy/datadog/smoketest/LogInjectionSmokeTest.groovy @@ -31,7 +31,8 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { static final int TIMEOUT_SECS = 30 static final String LOG4J2_BACKEND = "Log4j2" - static final Set DIRECT_LOG_SUBMISSION_BACKENDS = [LOG4J2_BACKEND, "Logback"] as Set + static final String LOGBACK_BACKEND = "Logback" + static final Set DIRECT_LOG_SUBMISSION_BACKENDS = [LOG4J2_BACKEND, LOGBACK_BACKEND] as Set @Shared File outputLogFile @@ -597,7 +598,7 @@ class Log4j2InterfaceLog4j2LatestBackend extends Log4j2InterfaceLog4j2Backend {} class Slf4jInterfaceLogbackBackend extends LogInjectionSmokeTest { def backend() { - "Logback" + LOGBACK_BACKEND } } @@ -704,7 +705,7 @@ class JULInterfaceSlf4jToLogbackBackend extends LogInjectionSmokeTest { def propertiesFile = File.createTempFile("julConfig", ".properties") def backend() { - "Logback" + LOGBACK_BACKEND } def setupSpec() { @@ -731,7 +732,7 @@ class JULInterfaceSlf4jToLogbackLatestBackend extends JULInterfaceSlf4jToLogback class JCLInterfaceSlf4jToLogbackBackend extends LogInjectionSmokeTest { def backend() { - "Logback" + LOGBACK_BACKEND } } @@ -741,7 +742,7 @@ class JCLInterfaceSlf4jToLogbackLatestBackend extends JCLInterfaceSlf4jToLogback class Log4j1InterfaceSlf4jToLogbackBackend extends LogInjectionSmokeTest { def backend() { - "Logback" + LOGBACK_BACKEND } } @@ -751,7 +752,7 @@ class Log4j1InterfaceSlf4jToLogbackLatestBackend extends Log4j1InterfaceSlf4jToL class Log4j2InterfaceSlf4jToLogbackBackend extends LogInjectionSmokeTest { def backend() { - "Logback" + LOGBACK_BACKEND } } @@ -801,7 +802,7 @@ class JBossInterfaceLog4j2LatestBackend extends JBossInterfaceLog4j2Backend {} class JBossInterfaceSlf4jToLogbackBackend extends LogInjectionSmokeTest { def backend() { - "Logback" + LOGBACK_BACKEND } } @@ -815,7 +816,7 @@ class FloggerInterfaceJULBackend extends JULBackend {} class FloggerInterfaceSlf4jToLogbackBackend extends LogInjectionSmokeTest { def backend() { - "Logback" + LOGBACK_BACKEND } List additionalArguments() { From 4cb273ab383e8e8b400a92339c4e2c2d0826356d Mon Sep 17 00:00:00 2001 From: Daniel Mohedano Date: Tue, 6 Oct 2026 13:12:55 +0200 Subject: [PATCH 5/5] fix(logback): submit no events for a FATAL submission level --- .../trace/instrumentation/logback/LogsIntakeHelper.java | 7 ++++++- .../src/test/java/LogbackLogsIntakeForkedTest.java | 9 ++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogsIntakeHelper.java b/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogsIntakeHelper.java index 6aa961a6c36..f75f388e6a6 100644 --- a/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogsIntakeHelper.java +++ b/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogsIntakeHelper.java @@ -17,7 +17,7 @@ public class LogsIntakeHelper { private static final boolean APP_LOGS_COLLECTION = Config.get().isAppLogsCollectionEnabled(); private static final Level SUBMISSION_LEVEL = - Level.toLevel(Config.get().getAgentlessLogSubmissionLevel(), Level.INFO); + submissionLevel(Config.get().getAgentlessLogSubmissionLevel()); public static void log(ILoggingEvent event) { // App-log collection keeps its existing payload and framework-level filtering, even when @@ -63,6 +63,11 @@ private static Map map(ILoggingEvent event) { return log; } + /** Logback has no FATAL level, and ERROR is below a FATAL threshold. */ + private static Level submissionLevel(String level) { + return "FATAL".equalsIgnoreCase(level) ? Level.OFF : Level.toLevel(level, Level.INFO); + } + private static void addCorrelationIds(Map log) { String traceId = CorrelationIdentifier.getTraceId(); if (traceId != null && !traceId.equals("0")) { diff --git a/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackLogsIntakeForkedTest.java b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackLogsIntakeForkedTest.java index 2ab02261479..7e219e574a4 100644 --- a/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackLogsIntakeForkedTest.java +++ b/dd-java-agent/instrumentation/logback-1.0/src/test/java/LogbackLogsIntakeForkedTest.java @@ -86,8 +86,12 @@ static boolean integration() { return InstrumenterConfig.get().isIntegrationEnabled(singletonList("logback"), true); } + static boolean fatalLevel() { + return "FATAL".equals(Config.get().getAgentlessLogSubmissionLevel()); + } + static boolean collected() { - return integration() && (agentless() || appLogs()); + return integration() && (appLogs() || (agentless() && !fatalLevel())); } @Test @@ -227,6 +231,9 @@ class LogbackAppLogsForkedTest extends AbstractLogbackLogsIntakeForkedTest {} @WithConfig(key = APP_LOGS_COLLECTION_ENABLED, value = "true") class LogbackBothFlagsForkedTest extends AbstractLogbackLogsIntakeForkedTest {} +@WithConfig(key = AGENTLESS_LOG_SUBMISSION_LEVEL, value = "FATAL") +class LogbackFatalLevelForkedTest extends AbstractLogbackLogsIntakeForkedTest {} + @WithConfig(key = LOGS_INJECTION_ENABLED, value = "false") class LogbackNoInjectionForkedTest extends AbstractLogbackLogsIntakeForkedTest {}