From d32101ecd7bb14af4c920e5afa21d94963c56bf6 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Tue, 2 Dec 2025 15:29:36 +0100 Subject: [PATCH 1/2] Add Logs Capture feature Add application logs collection based on log framework instrumentation that hooks when log event are generated to be written. Log events are sent to Logs backend through Datadog agent's Event Platform (Evp) proxy. We are reusing a previous infrastructure used for uploading logs for CI Visibility (see #7082). We are adding support for Logback. This feature is opt-in through DD_APP_LOGS_COLLECTION_ENABLED config. Logs are sent as structured log layout and allow to add any additional attributes. For now we are adding directly trace correlation ids (traceId and spanId) and give the logs injection feature for free and without any setup on the log framework side. Smoke tests are based on Logs Injection tests. # Conflicts: # metadata/supported-configurations.json --- .../java/datadog/trace/bootstrap/Agent.java | 11 +- .../trace/logging/intake/LogsDispatcher.java | 3 +- .../logging/intake/LogsIntakeSystem.java | 9 +- .../trace/logging/intake/LogsWriterImpl.java | 6 +- .../log4j2/DatadogAppender.java | 40 +++-- .../log4j2/LoggerConfigInstrumentation.java | 9 +- ...DatadogAppenderAppLogCollectionTest.groovy | 70 ++++++++ .../logback/LogbackLoggerInstrumentation.java | 30 +++- .../logback/LogsIntakeHelper.java | 45 +++++ .../smoketest/LogInjectionSmokeTest.groovy | 159 ++++++++++++++++-- .../OpenFeatureProviderSmokeTest.groovy | 5 +- .../smoketest/AbstractSmokeTest.groovy | 3 +- .../trace/api/config/GeneralConfig.java | 1 + .../java/datadog/trace/core/StatusLogger.java | 2 + .../servicediscovery/ServiceDiscovery.java | 7 +- .../ServiceDiscoveryTest.groovy | 13 +- .../main/java/datadog/trace/api/Config.java | 7 + metadata/supported-configurations.json | 8 + 18 files changed, 378 insertions(+), 50 deletions(-) create mode 100644 dd-java-agent/instrumentation/log4j/log4j-2.0/src/test/groovy/Log4jDatadogAppenderAppLogCollectionTest.groovy create mode 100644 dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogsIntakeHelper.java diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index f3b5da141fc..657ef935e25 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -47,6 +47,7 @@ import datadog.trace.api.gateway.SubscriptionService; import datadog.trace.api.git.EmbeddedGitInfoBuilder; import datadog.trace.api.git.GitInfoProvider; +import datadog.trace.api.intake.Intake; import datadog.trace.api.profiling.ProfilingEnablement; import datadog.trace.api.scopemanager.ScopeListener; import datadog.trace.bootstrap.benchmark.StaticEventLogger; @@ -129,6 +130,7 @@ private enum AgentFeature { CODE_ORIGIN(TraceInstrumentationConfig.CODE_ORIGIN_FOR_SPANS_ENABLED, false), DATA_JOBS(GeneralConfig.DATA_JOBS_ENABLED, false), AGENTLESS_LOG_SUBMISSION(GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED, false), + APP_LOGS_COLLECTION(GeneralConfig.APP_LOGS_COLLECTION_ENABLED, false), LLMOBS(LlmObsConfig.LLMOBS_ENABLED, false), LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false), FEATURE_FLAGGING(FeatureFlaggingConfig.FLAGGING_PROVIDER_ENABLED, false); @@ -190,6 +192,7 @@ public boolean isEnabledByDefault() { private static boolean codeOriginEnabled = false; private static boolean distributedDebuggerEnabled = false; private static boolean agentlessLogSubmissionEnabled = false; + private static boolean appLogsCollectionEnabled = false; private static boolean featureFlaggingEnabled = false; private static void safelySetContextClassLoader(ClassLoader classLoader) { @@ -275,6 +278,7 @@ public static void start( exceptionReplayEnabled = isFeatureEnabled(AgentFeature.EXCEPTION_REPLAY); codeOriginEnabled = isFeatureEnabled(AgentFeature.CODE_ORIGIN); agentlessLogSubmissionEnabled = isFeatureEnabled(AgentFeature.AGENTLESS_LOG_SUBMISSION); + appLogsCollectionEnabled = isFeatureEnabled(AgentFeature.APP_LOGS_COLLECTION); llmObsEnabled = isFeatureEnabled(AgentFeature.LLMOBS); featureFlaggingEnabled = isFeatureEnabled(AgentFeature.FEATURE_FLAGGING); @@ -1131,15 +1135,16 @@ private static void maybeStartFeatureFlagging(final Class scoClass, final Obj } private static void maybeInstallLogsIntake(Class scoClass, Object sco) { - if (agentlessLogSubmissionEnabled) { + if (agentlessLogSubmissionEnabled || appLogsCollectionEnabled) { StaticEventLogger.begin("Logs Intake"); try { final Class logsIntakeSystemClass = AGENT_CLASSLOADER.loadClass("datadog.trace.logging.intake.LogsIntakeSystem"); final Method logsIntakeInstallerMethod = - logsIntakeSystemClass.getMethod("install", scoClass); - logsIntakeInstallerMethod.invoke(null, sco); + logsIntakeSystemClass.getMethod("install", scoClass, Intake.class); + logsIntakeInstallerMethod.invoke( + null, sco, agentlessLogSubmissionEnabled ? Intake.LOGS : Intake.EVENT_PLATFORM); } catch (final Throwable e) { log.warn("Not installing Logs Intake subsystem", e); } diff --git a/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsDispatcher.java b/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsDispatcher.java index 79e9ae580d1..03a0a24c734 100644 --- a/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsDispatcher.java +++ b/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsDispatcher.java @@ -85,7 +85,8 @@ public void dispatch(List> messages) { private void flush(StringBuilder batch) { try { - RequestBody requestBody = RequestBody.create(JSON, batch.toString()); + String json = batch.toString(); + RequestBody requestBody = RequestBody.create(JSON, json); RequestBody gzippedRequestBody = OkHttpUtils.gzippedRequestBodyOf(requestBody); backendApi.post("logs", gzippedRequestBody, IGNORE_RESPONSE, null, true); } catch (IOException e) { diff --git a/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsIntakeSystem.java b/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsIntakeSystem.java index bea8cb802f2..d068c78deb0 100644 --- a/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsIntakeSystem.java +++ b/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsIntakeSystem.java @@ -3,6 +3,7 @@ import datadog.communication.BackendApiFactory; import datadog.communication.ddagent.SharedCommunicationObjects; import datadog.trace.api.Config; +import datadog.trace.api.intake.Intake; import datadog.trace.api.logging.intake.LogsIntake; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,15 +12,15 @@ public class LogsIntakeSystem { private static final Logger LOGGER = LoggerFactory.getLogger(LogsIntakeSystem.class); - public static void install(SharedCommunicationObjects sco) { + public static void install(SharedCommunicationObjects sco, Intake intake) { Config config = Config.get(); - if (!config.isAgentlessLogSubmissionEnabled()) { - LOGGER.debug("Agentless logs intake is disabled"); + if (!config.isAgentlessLogSubmissionEnabled() && !config.isAppLogsCollectionEnabled()) { + LOGGER.debug("Agentless logs intake and logs capture are disabled"); return; } BackendApiFactory apiFactory = new BackendApiFactory(config, sco); - LogsWriterImpl writer = new LogsWriterImpl(config, apiFactory); + LogsWriterImpl writer = new LogsWriterImpl(config, apiFactory, intake); sco.whenReady(writer::start); LogsIntake.registerWriter(writer); diff --git a/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsWriterImpl.java b/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsWriterImpl.java index c36950f422c..6d88d0e0a59 100644 --- a/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsWriterImpl.java +++ b/dd-java-agent/agent-logs-intake/src/main/java/datadog/trace/logging/intake/LogsWriterImpl.java @@ -27,11 +27,13 @@ public class LogsWriterImpl implements LogsWriter { private final Map commonTags; private final BackendApiFactory apiFactory; + private final Intake intake; private final BlockingQueue> messageQueue; private final Thread messagePollingThread; - public LogsWriterImpl(Config config, BackendApiFactory apiFactory) { + public LogsWriterImpl(Config config, BackendApiFactory apiFactory, Intake intake) { this.apiFactory = apiFactory; + this.intake = intake; commonTags = new HashMap<>(); commonTags.put("ddsource", "java"); @@ -87,7 +89,7 @@ public void log(Map message) { } private void logPollingLoop() { - BackendApi backendApi = apiFactory.createBackendApi(Intake.LOGS); + BackendApi backendApi = apiFactory.createBackendApi(intake); LogsDispatcher logsDispatcher = new LogsDispatcher(backendApi); while (!Thread.currentThread().isInterrupted()) { diff --git a/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/DatadogAppender.java b/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/DatadogAppender.java index dd5852112dd..5a151f90d11 100644 --- a/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/DatadogAppender.java +++ b/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/DatadogAppender.java @@ -1,5 +1,7 @@ package datadog.trace.instrumentation.log4j2; +import datadog.trace.api.Config; +import datadog.trace.api.CorrelationIdentifier; import datadog.trace.api.logging.intake.LogsIntake; import java.io.PrintWriter; import java.io.StringWriter; @@ -13,8 +15,11 @@ public class DatadogAppender extends AbstractAppender { private static final int MAX_STACKTRACE_STRING_LENGTH = 16 * 1_024; - public DatadogAppender(String name, Filter filter) { + private final boolean appLogsCollectionEnabled; + + public DatadogAppender(String name, Filter filter, Config config) { super(name, filter, null); + this.appLogsCollectionEnabled = config.isAppLogsCollectionEnabled(); } @Override @@ -40,7 +45,6 @@ private Map map(final LogEvent event) { Map thrownLog = new HashMap<>(); thrownLog.put("message", thrown.getMessage()); thrownLog.put("name", thrown.getClass().getCanonicalName()); - // TODO consider using structured stack trace layout // (see // org.apache.logging.log4j.layout.template.json.resolver.ExceptionResolver#createStackTraceResolver) @@ -55,19 +59,29 @@ private Map map(final LogEvent event) { log.put("thrown", thrownLog); } - - log.put("contextMap", event.getContextMap()); log.put("endOfBatch", event.isEndOfBatch()); log.put("loggerFqcn", event.getLoggerFqcn()); - - StackTraceElement source = event.getSource(); - Map sourceLog = new HashMap<>(); - sourceLog.put("class", source.getClassName()); - sourceLog.put("method", source.getMethodName()); - sourceLog.put("file", source.getFileName()); - sourceLog.put("line", source.getLineNumber()); - log.put("source", sourceLog); - + if (appLogsCollectionEnabled) { + // skip log source for now as this is expensive + // will be later introduce with Log Origin and optimisations + String traceId = CorrelationIdentifier.getTraceId(); + if (traceId != null && !traceId.equals("0")) { + log.put("dd.trace_id", traceId); + } + String spanId = CorrelationIdentifier.getSpanId(); + if (spanId != null && !spanId.equals("0")) { + log.put("dd.span_id", spanId); + } + } else { + log.put("contextMap", event.getContextMap()); + StackTraceElement source = event.getSource(); + Map sourceLog = new HashMap<>(); + sourceLog.put("class", source.getClassName()); + sourceLog.put("method", source.getMethodName()); + sourceLog.put("file", source.getFileName()); + sourceLog.put("line", source.getLineNumber()); + log.put("source", sourceLog); + } return log; } } diff --git a/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/LoggerConfigInstrumentation.java b/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/LoggerConfigInstrumentation.java index d4c50e4c5ab..264e54dd216 100644 --- a/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/LoggerConfigInstrumentation.java +++ b/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/LoggerConfigInstrumentation.java @@ -24,8 +24,9 @@ public LoggerConfigInstrumentation() { @Override public boolean isApplicable(Set enabledSystems) { - return super.isApplicable(enabledSystems) - && InstrumenterConfig.get().isAgentlessLogSubmissionEnabled(); + return (super.isApplicable(enabledSystems) + && InstrumenterConfig.get().isAgentlessLogSubmissionEnabled()) + || Config.get().isAppLogsCollectionEnabled(); } @Override @@ -62,10 +63,10 @@ public static void onExit(@Advice.This LoggerConfig loggerConfig) { } } - DatadogAppender appender = new DatadogAppender("datadog", null); + Config config = Config.get(); + DatadogAppender appender = new DatadogAppender("datadog", null, config); appender.start(); - Config config = Config.get(); Level level = Level.valueOf(config.getAgentlessLogSubmissionLevel()); loggerConfig.addAppender(appender, level, null); } diff --git a/dd-java-agent/instrumentation/log4j/log4j-2.0/src/test/groovy/Log4jDatadogAppenderAppLogCollectionTest.groovy b/dd-java-agent/instrumentation/log4j/log4j-2.0/src/test/groovy/Log4jDatadogAppenderAppLogCollectionTest.groovy new file mode 100644 index 00000000000..b61db2268ce --- /dev/null +++ b/dd-java-agent/instrumentation/log4j/log4j-2.0/src/test/groovy/Log4jDatadogAppenderAppLogCollectionTest.groovy @@ -0,0 +1,70 @@ +import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.api.config.GeneralConfig +import datadog.trace.api.logging.intake.LogsIntake +import datadog.trace.api.logging.intake.LogsWriter +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.appender.AbstractAppender +import org.junit.jupiter.api.Assumptions +import spock.util.environment.Jvm + +class Log4jDatadogAppenderAppLogCollectionTest extends InstrumentationSpecification { + + private static DummyLogsWriter logsWriter + + def setupSpec() { + logsWriter = new DummyLogsWriter() + LogsIntake.registerWriter(logsWriter) + } + + @Override + void configurePreAgent() { + super.configurePreAgent() + injectSysConfig(GeneralConfig.APP_LOGS_COLLECTION_ENABLED, "true") + } + + def "test datadog appender registration"() { + setup: + ensureLog4jVersionCompatibleWithCurrentJVM() + + def logger = LogManager.getLogger(Log4jDatadogAppenderAppLogCollectionTest) + + when: + logger.error("A test message") + + then: + !logsWriter.messages.empty + + def message = logsWriter.messages.poll() + "A test message" == message.get("message") + "ERROR" == message.get("level") + "Log4jDatadogAppenderAppLogCollectionTest" == message.get("loggerName") + } + + private static ensureLog4jVersionCompatibleWithCurrentJVM() { + try { + // init class to see if UnsupportedClassVersionError gets thrown + AbstractAppender.package + } catch (UnsupportedClassVersionError e) { + Assumptions.assumeTrue(false, "Latest Log4j2 release requires Java 17, current JVM: " + Jvm.current.javaVersion) + } + } + + private static final class DummyLogsWriter implements LogsWriter { + private final Queue> messages = new ArrayDeque<>() + + @Override + void log(Map message) { + messages.offer(message) + } + + @Override + void start() { + // no op + } + + @Override + void shutdown() { + // no op + } + } +} 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 09cf9772c34..b7ac7fb0228 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 @@ -18,10 +18,12 @@ import com.google.auto.service.AutoService; import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.Config; import datadog.trace.bootstrap.InstrumentationContext; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; import java.util.Map; +import java.util.Set; import net.bytebuddy.asm.Advice; @AutoService(InstrumenterModule.class) @@ -29,7 +31,7 @@ public class LogbackLoggerInstrumentation extends InstrumenterModule.Tracing implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { public LogbackLoggerInstrumentation() { - super("logback"); + super("logback", "logs-intake", "logs-intake-logback"); } @Override @@ -43,6 +45,16 @@ public Map contextStore() { "ch.qos.logback.classic.spi.ILoggingEvent", AgentSpanContext.class.getName()); } + @Override + public boolean isApplicable(Set enabledSystems) { + return super.isApplicable(enabledSystems) || Config.get().isAppLogsCollectionEnabled(); + } + + @Override + public String[] helperClassNames() { + return new String[] {LogsIntakeHelper.class.getName()}; + } + @Override public void methodAdvice(MethodTransformer transformer) { transformer.applyAdvice( @@ -52,6 +64,15 @@ public void methodAdvice(MethodTransformer transformer) { .and(takesArguments(1)) .and(takesArgument(0, named("ch.qos.logback.classic.spi.ILoggingEvent"))), LogbackLoggerInstrumentation.class.getName() + "$CallAppendersAdvice"); + if (Config.get().isAppLogsCollectionEnabled()) { + transformer.applyAdvice( + isMethod() + .and(isPublic()) + .and(named("callAppenders")) + .and(takesArguments(1)) + .and(takesArgument(0, named("ch.qos.logback.classic.spi.ILoggingEvent"))), + LogbackLoggerInstrumentation.class.getName() + "$CallAppendersAdvice2"); + } } public static class CallAppendersAdvice { @@ -65,4 +86,11 @@ public static void onEnter(@Advice.Argument(0) ILoggingEvent event) { } } } + + public static class CallAppendersAdvice2 { + @Advice.OnMethodEnter + public static void onEnter(@Advice.Argument(0) ILoggingEvent event) { + LogsIntakeHelper.log(event); + } + } } 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 new file mode 100644 index 00000000000..593acd82508 --- /dev/null +++ b/dd-java-agent/instrumentation/logback-1.0/src/main/java/datadog/trace/instrumentation/logback/LogsIntakeHelper.java @@ -0,0 +1,45 @@ +package datadog.trace.instrumentation.logback; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.StackTraceElementProxy; +import datadog.trace.api.CorrelationIdentifier; +import datadog.trace.api.logging.intake.LogsIntake; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class LogsIntakeHelper { + + public static void log(ILoggingEvent event) { + LogsIntake.log(map(event)); + } + + private static Map map(ILoggingEvent event) { + Map log = new HashMap<>(); + log.put("thread", event.getThreadName()); + log.put("level", event.getLevel().levelStr); + log.put("loggerName", event.getLoggerName()); + log.put("message", event.getFormattedMessage()); + if (event.getThrowableProxy() != null) { + Map thrownLog = new HashMap<>(); + thrownLog.put("message", event.getThrowableProxy().getMessage()); + thrownLog.put("name", event.getThrowableProxy().getClassName()); + String stackTraceString = + Arrays.stream(event.getThrowableProxy().getStackTraceElementProxyArray()) + .map(StackTraceElementProxy::getSTEAsString) + .collect(Collectors.joining(" ")); + thrownLog.put("extendedStackTrace", stackTraceString); + log.put("thrown", thrownLog); + } + String traceId = CorrelationIdentifier.getTraceId(); + if (traceId != null && !traceId.equals("0")) { + log.put("dd.trace_id", traceId); + } + String spanId = CorrelationIdentifier.getSpanId(); + if (spanId != null && !spanId.equals("0")) { + log.put("dd.span_id", spanId); + } + return log; + } +} 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 297351a2aac..c5548dd2686 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 @@ -3,8 +3,11 @@ package datadog.smoketest import com.squareup.moshi.Moshi import com.squareup.moshi.Types import datadog.environment.JavaVirtualMachine +import datadog.trace.agent.test.server.http.TestHttpServer.HandlerApi.RequestApi import datadog.trace.api.config.GeneralConfig import datadog.trace.test.util.Flaky +import java.nio.charset.StandardCharsets +import java.util.zip.GZIPInputStream import spock.lang.AutoCleanup import spock.lang.Shared @@ -43,6 +46,9 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { @Shared boolean trace128bits = true + @Shared + boolean appLogCollection = false + @Shared @AutoCleanup MockBackend mockBackend = new MockBackend() @@ -62,6 +68,10 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { if (trace128bits) { jarName = jarName.substring(0, jarName.length() - 7) } + appLogCollection = jarName.endsWith("AppLogCollection") + if (appLogCollection) { + jarName = jarName.substring(0, jarName.length() - "AppLogCollection".length()) + } def loggingJar = buildDirectory + "/libs/" + jarName + ".jar" assert new File(loggingJar).isFile() @@ -75,7 +85,9 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { // turn off these features as their debug output can break up our expected logging lines on IBM JVMs // causing random test failures (we are not testing these features here so they don't need to be on) command.add("-Ddd.instrumentation.telemetry.enabled=false") - command.removeAll { it.startsWith("-Ddd.profiling")} + command.removeAll { + it.startsWith("-Ddd.profiling") + } command.add("-Ddd.profiling.enabled=false") command.add("-Ddd.remote_config.enabled=true") command.add("-Ddd.remote_config.url=http://localhost:${server.address.port}/v0.7/config".toString()) @@ -96,6 +108,9 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { command.add("-Ddd.$GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED=true" as String) command.add("-Ddd.$GeneralConfig.AGENTLESS_LOG_SUBMISSION_URL=${mockBackend.intakeUrl}" as String) } + if (supportsAppLogCollection()) { + command.add("-Ddd.$GeneralConfig.APP_LOGS_COLLECTION_ENABLED=true" as String) + } command.addAll(additionalArguments()) command.addAll((String[]) ["-jar", loggingJar]) @@ -126,6 +141,37 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { return "debug" } + @Override + Closure decodedEvpProxyMessageCallback() { + return { + String path, RequestApi request -> + try { + boolean isCompressed = request.getHeader("Content-Encoding").contains("gzip") + byte[] body = request.body + if (body != null) { + if (isCompressed) { + ByteArrayOutputStream output = new ByteArrayOutputStream() + byte[] buffer = new byte[4096] + try (GZIPInputStream input = new GZIPInputStream(new ByteArrayInputStream(body))) { + int bytesRead = input.read(buffer, 0, buffer.length) + output.write(buffer, 0, bytesRead) + } + body = output.toByteArray() + } + final strBody = new String(body, StandardCharsets.UTF_8) + println("evp mesg: " + strBody) + final jsonAdapter = new Moshi.Builder().build().adapter(Types.newParameterizedType(List, Types.newParameterizedType(Map, String, Object))) + List> msg = jsonAdapter.fromJson(strBody) + msg + } + } catch (Throwable t) { + println("=== Failure during EvP proxy decoding ===") + t.printStackTrace(System.out) + throw t + } + } + } + List additionalArguments() { return [] } @@ -142,6 +188,10 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { return backend() == LOG4J2_BACKEND } + def supportsAppLogCollection() { + false + } + abstract backend() def cleanupSpec() { @@ -150,10 +200,12 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { } def assertRawLogLinesWithoutInjection(List logLines, String firstTraceId, String firstSpanId, String secondTraceId, String secondSpanId, - String thirdTraceId, String thirdSpanId, String forthTraceId, String forthSpanId) { + String thirdTraceId, String thirdSpanId, String forthTraceId, String forthSpanId) { // Assert log line starts with backend name. // This avoids tests inadvertently passing because the incorrect backend is logging - logLines.every { it.startsWith(backend())} + logLines.every { + it.startsWith(backend()) + } assert logLines.size() == 7 assert logLines[0].endsWith("- BEFORE FIRST SPAN") assert logLines[1].endsWith("- INSIDE FIRST SPAN") @@ -167,10 +219,12 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { } def assertRawLogLinesWithInjection(List logLines, String firstTraceId, String firstSpanId, String secondTraceId, String secondSpanId, - String thirdTraceId, String thirdSpanId, String forthTraceId, String forthSpanId) { + String thirdTraceId, String thirdSpanId, String forthTraceId, String forthSpanId) { // Assert log line starts with backend name. // This avoids tests inadvertently passing because the incorrect backend is logging - logLines.every { it.startsWith(backend()) } + logLines.every { + it.startsWith(backend()) + } def tagsPart = noTags ? " " : "${SERVICE_NAME} ${ENV} ${VERSION}" assert logLines.size() == 7 assert logLines[0].endsWith("- ${tagsPart} - BEFORE FIRST SPAN") || logLines[0].endsWith("- ${tagsPart} 0 0 - BEFORE FIRST SPAN") @@ -184,23 +238,33 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { } def assertJsonLinesWithInjection(List rawLines, String firstTraceId, String firstSpanId, String secondTraceId, String secondSpanId, - String thirdTraceId, String thirdSpanId, String forthTraceId, String forthSpanId) { - def logLines = rawLines.collect { println it; jsonAdapter.fromJson(it) as Map} + String thirdTraceId, String thirdSpanId, String forthTraceId, String forthSpanId) { + def logLines = rawLines.collect { + println it; jsonAdapter.fromJson(it) as Map + } assert logLines.size() == 7 // Log4j2's KeyValuePair for injecting static values into Json only exists in later versions of Log4j2 // Its tested with Log4j2LatestBackend if (!getClass().simpleName.contains("Log4j2Backend")) { - assert logLines.every { it["backend"] == backend() } + assert logLines.every { + it["backend"] == backend() + } } return assertParsedJsonLinesWithInjection(logLines, firstTraceId, firstSpanId, secondTraceId, secondSpanId, forthTraceId, forthSpanId) } private assertParsedJsonLinesWithInjection(List logLines, String firstTraceId, String firstSpanId, String secondTraceId, String secondSpanId, String forthTraceId, String forthSpanId) { - assert logLines.every { getFromContext(it, "dd.service") == noTags ? null : SERVICE_NAME } - assert logLines.every { getFromContext(it, "dd.version") == noTags ? null : VERSION } - assert logLines.every { getFromContext(it, "dd.env") == noTags ? null : ENV } + assert logLines.every { + getFromContext(it, "dd.service") == noTags ? null : SERVICE_NAME + } + assert logLines.every { + getFromContext(it, "dd.version") == noTags ? null : VERSION + } + assert logLines.every { + getFromContext(it, "dd.env") == noTags ? null : ENV + } assert getFromContext(logLines[0], "dd.trace_id") == null assert getFromContext(logLines[0], "dd.span_id") == null @@ -233,6 +297,48 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { return true } + private assertParsedJsonLinesWithAppLogCollection(List logLines, String firstTraceId, String firstSpanId, String secondTraceId, String secondSpanId, String thirdTraceId, String thirdSpanId, String forthTraceId, String forthSpanId) { + assert logLines.every { + getFromContext(it, "dd.service") == noTags ? null : SERVICE_NAME + } + assert logLines.every { + getFromContext(it, "dd.version") == noTags ? null : VERSION + } + assert logLines.every { + getFromContext(it, "dd.env") == noTags ? null : ENV + } + + assert getFromContext(logLines[0], "dd.trace_id") == null + assert getFromContext(logLines[0], "dd.span_id") == null + assert logLines[0]["message"] == "BEFORE FIRST SPAN" + + assert getFromContext(logLines[1], "dd.trace_id") == firstTraceId + assert getFromContext(logLines[1], "dd.span_id") == firstSpanId + assert logLines[1]["message"] == "INSIDE FIRST SPAN" + + assert getFromContext(logLines[2], "dd.trace_id") == null + assert getFromContext(logLines[2], "dd.span_id") == null + assert logLines[2]["message"] == "AFTER FIRST SPAN" + + assert getFromContext(logLines[3], "dd.trace_id") == secondTraceId + assert getFromContext(logLines[3], "dd.span_id") == secondSpanId + assert logLines[3]["message"] == "INSIDE SECOND SPAN" + + assert getFromContext(logLines[4], "dd.trace_id") == thirdTraceId + assert getFromContext(logLines[4], "dd.span_id") == thirdSpanId + assert logLines[4]["message"] == "INSIDE THIRD SPAN" + + assert getFromContext(logLines[5], "dd.trace_id") == forthTraceId + assert getFromContext(logLines[5], "dd.span_id") == forthSpanId + assert logLines[5]["message"] == "INSIDE FORTH SPAN" + + assert getFromContext(logLines[6], "dd.trace_id") == null + assert getFromContext(logLines[6], "dd.span_id") == null + assert logLines[6]["message"] == "AFTER FORTH SPAN" + + return true + } + def getFromContext(Map logEvent, String key) { if (logEvent["contextMap"] != null) { return logEvent["contextMap"][key] @@ -326,6 +432,13 @@ abstract class LogInjectionSmokeTest extends AbstractSmokeTest { if (supportsDirectLogSubmission()) { assertParsedJsonLinesWithInjection(mockBackend.waitForLogs(7), firstTraceId, firstSpanId, secondTraceId, secondSpanId, forthTraceId, forthSpanId) } + + if (supportsAppLogCollection()) { + def lines = evpProxyMessages.collect { + it.v2 + }.flatten() as List> + assertParsedJsonLinesWithAppLogCollection(lines, firstTraceId, firstSpanId, secondTraceId, secondSpanId, thirdTraceId, thirdSpanId, forthTraceId, forthSpanId) + } } void checkTraceIdFormat(String traceId) { @@ -486,6 +599,18 @@ class Slf4jInterfaceLogbackBackend extends LogInjectionSmokeTest { } } +class Slf4jInterfaceLogbackBackendAppLogCollection extends Slf4jInterfaceLogbackBackend { + @Override + def supportsDirectLogSubmission() { + false + } + + @Override + def supportsAppLogCollection() { + true + } +} + class Slf4jInterfaceLogbackBackendNoTags extends Slf4jInterfaceLogbackBackend {} class Slf4jInterfaceLogbackBackend128bTid extends Slf4jInterfaceLogbackBackend {} class Slf4jInterfaceLogbackLatestBackend extends Slf4jInterfaceLogbackBackend {} @@ -512,6 +637,18 @@ class Slf4jInterfaceLog4j2Backend extends LogInjectionSmokeTest { class Slf4jInterfaceLog4j2BackendNoTags extends Slf4jInterfaceLog4j2Backend {} class Slf4jInterfaceLog4j2Backend128bTid extends Slf4jInterfaceLog4j2Backend {} class Slf4jInterfaceLog4j2LatestBackend extends Slf4jInterfaceLog4j2Backend {} +class Slf4jInterfaceLog4j2BackendAppLogCollection extends Slf4jInterfaceLog4j2Backend { + @Override + def supportsDirectLogSubmission() { + false + } + + @Override + def supportsAppLogCollection() { + true + } +} + class Slf4jInterfaceSlf4jSimpleBackend extends LogInjectionSmokeTest { def backend() { diff --git a/dd-smoke-tests/openfeature/src/test/groovy/datadog/smoketest/springboot/OpenFeatureProviderSmokeTest.groovy b/dd-smoke-tests/openfeature/src/test/groovy/datadog/smoketest/springboot/OpenFeatureProviderSmokeTest.groovy index b2e8539c2e8..9b918acc5c4 100644 --- a/dd-smoke-tests/openfeature/src/test/groovy/datadog/smoketest/springboot/OpenFeatureProviderSmokeTest.groovy +++ b/dd-smoke-tests/openfeature/src/test/groovy/datadog/smoketest/springboot/OpenFeatureProviderSmokeTest.groovy @@ -3,6 +3,7 @@ package datadog.smoketest.springboot import datadog.remoteconfig.Capabilities import datadog.remoteconfig.Product import datadog.smoketest.AbstractServerSmokeTest +import datadog.trace.agent.test.server.http.TestHttpServer.HandlerApi.RequestApi import groovy.json.JsonOutput import groovy.json.JsonSlurper import java.nio.file.Files @@ -41,11 +42,11 @@ class OpenFeatureProviderSmokeTest extends AbstractServerSmokeTest { @Override Closure decodedEvpProxyMessageCallback() { - return { String path, byte[] body -> + return { String path, RequestApi request -> if (!path.contains('api/v2/exposures')) { return null } - return new JsonSlurper().parse(body) + return new JsonSlurper().parse(request.body) } } diff --git a/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy b/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy index e0d675fe5a1..8a246ed0c03 100644 --- a/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy +++ b/dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy @@ -178,8 +178,7 @@ abstract class AbstractSmokeTest extends ProcessManager { prefix("/evp_proxy/v2/") { try { final path = request.path.toString() - final body = request.getBody() - final decoded = decodeEvpMessage?.call(path, body) + final decoded = decodeEvpMessage?.call(path, request) if (decoded) { evpProxyMessages.add(new Tuple2<>(path, decoded)) } diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java index 2e441e2677b..3d58a5d21ed 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java @@ -120,6 +120,7 @@ public final class GeneralConfig { public static final String SSI_INJECTION_ENABLED = "injection.enabled"; public static final String SSI_INJECTION_FORCE = "inject.force"; public static final String INSTRUMENTATION_SOURCE = "instrumentation.source"; + public static final String APP_LOGS_COLLECTION_ENABLED = "app.logs.collection.enabled"; private GeneralConfig() {} } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/StatusLogger.java b/dd-trace-core/src/main/java/datadog/trace/core/StatusLogger.java index d7507503b88..502e246e10f 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/StatusLogger.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/StatusLogger.java @@ -156,6 +156,8 @@ public void toJson(JsonWriter writer, Config config) throws IOException { } writer.name("data_streams_enabled"); writer.value(config.isDataStreamsEnabled()); + writer.name("app_logs_collection_enabled"); + writer.value(config.isAppLogsCollectionEnabled()); writer.endObject(); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ServiceDiscovery.java b/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ServiceDiscovery.java index 0af1e8fd020..a121c7543c1 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ServiceDiscovery.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ServiceDiscovery.java @@ -29,6 +29,7 @@ public void writeTracerMetadata(Config config) { ServiceDiscovery.encodePayload( TracerVersion.TRACER_VERSION, config.getHostName(), + config.isAppLogsCollectionEnabled(), config.getRuntimeId(), config.getServiceName(), config.getEnv(), @@ -50,6 +51,7 @@ private static String generateFileName() { static byte[] encodePayload( String tracerVersion, String hostname, + boolean appLogsCollectionEnabled, String runtimeID, String service, String env, @@ -59,7 +61,7 @@ static byte[] encodePayload( GrowableBuffer buffer = new GrowableBuffer(1024); MsgPackWriter writer = new MsgPackWriter(buffer); - int mapElements = 4; + int mapElements = 5; mapElements += (runtimeID != null && !runtimeID.isEmpty()) ? 1 : 0; mapElements += (service != null && !service.isEmpty()) ? 1 : 0; mapElements += (env != null && !env.isEmpty()) ? 1 : 0; @@ -81,6 +83,9 @@ static byte[] encodePayload( writer.writeUTF8("hostname".getBytes(ISO_8859_1)); writer.writeUTF8(hostname.getBytes(ISO_8859_1)); + writer.writeUTF8("logs_collected".getBytes(ISO_8859_1)); + writer.writeBoolean(appLogsCollectionEnabled); + if (runtimeID != null && !runtimeID.isEmpty()) { writer.writeUTF8("runtime_id".getBytes(ISO_8859_1)); writer.writeUTF8(runtimeID.getBytes(ISO_8859_1)); diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/servicediscovery/ServiceDiscoveryTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/servicediscovery/ServiceDiscoveryTest.groovy index adf421a1c07..3e8386867b2 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/servicediscovery/ServiceDiscoveryTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/servicediscovery/ServiceDiscoveryTest.groovy @@ -19,15 +19,16 @@ class ServiceDiscoveryTest extends DDCoreSpecification { String serviceVersion = "1.1.1" UTF8BytesString processTags = UTF8BytesString.create("key1:val1,key2:val2") String containerID = "containerID" + boolean appLogsCollectionEnabled = true when: - byte[] out = ServiceDiscovery.encodePayload(tracerVersion, hostname, runtimeID, service, env, serviceVersion, processTags, containerID) + byte[] out = ServiceDiscovery.encodePayload(tracerVersion, hostname, appLogsCollectionEnabled, runtimeID, service, env, serviceVersion, processTags, containerID) MapValue map = MessagePack.newDefaultUnpacker(out).unpackValue().asMapValue() then: - map.size() == 10 + map.size() == 11 and: - map.toString() == '{"schema_version":2,"tracer_language":"java","tracer_version":"1.2.3","hostname":"test-host","runtime_id":"rid-123","service_name":"orders","service_env":"prod","service_version":"1.1.1","process_tags":"key1:val1,key2:val2","container_id":"containerID"}' + map.toString() == '{"schema_version":2,"tracer_language":"java","tracer_version":"1.2.3","hostname":"test-host","logs_collected":true,"runtime_id":"rid-123","service_name":"orders","service_env":"prod","service_version":"1.1.1","process_tags":"key1:val1,key2:val2","container_id":"containerID"}' } def "encodePayload only required fields"() { @@ -36,13 +37,13 @@ class ServiceDiscoveryTest extends DDCoreSpecification { String hostname = "my_host" when: - byte[] out = ServiceDiscovery.encodePayload(tracerVersion, hostname, null, null, null, null, null, null) + byte[] out = ServiceDiscovery.encodePayload(tracerVersion, hostname, false, null, null, null, null, null, null) MapValue map = MessagePack.newDefaultUnpacker(out).unpackValue().asMapValue() then: - map.size() == 4 + map.size() == 5 and: - map.toString() == '{"schema_version":2,"tracer_language":"java","tracer_version":"1.2.3","hostname":"my_host"}' + map.toString() == '{"schema_version":2,"tracer_language":"java","tracer_version":"1.2.3","hostname":"my_host","logs_collected":false}' } def "generateFileName"() { when: diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 502bad37e4b..bcf75e25b2c 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -349,6 +349,7 @@ import static datadog.trace.api.config.GeneralConfig.APPLICATION_KEY; import static datadog.trace.api.config.GeneralConfig.APPLICATION_KEY_FILE; import static datadog.trace.api.config.GeneralConfig.APP_KEY; +import static datadog.trace.api.config.GeneralConfig.APP_LOGS_COLLECTION_ENABLED; import static datadog.trace.api.config.GeneralConfig.AZURE_APP_SERVICES; import static datadog.trace.api.config.GeneralConfig.DATA_JOBS_EXPERIMENTAL_FEATURES_ENABLED; import static datadog.trace.api.config.GeneralConfig.DATA_JOBS_OPENLINEAGE_ENABLED; @@ -885,6 +886,7 @@ public static String getHostName() { private final boolean traceInferredProxyEnabled; private final int clockSyncPeriod; private final boolean logsInjectionEnabled; + private final boolean appLogsCollectionEnabled; private final String dogStatsDNamedPipe; private final int dogStatsDStartDelay; @@ -1862,6 +1864,7 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins configProvider.getBoolean( LOGS_INJECTION_ENABLED, DEFAULT_LOGS_INJECTION_ENABLED, LOGS_INJECTION); } + appLogsCollectionEnabled = configProvider.getBoolean(APP_LOGS_COLLECTION_ENABLED, false); dogStatsDNamedPipe = configProvider.getString(DOGSTATSD_NAMED_PIPE); @@ -3511,6 +3514,10 @@ public boolean isLogsInjectionEnabled() { return logsInjectionEnabled; } + public boolean isAppLogsCollectionEnabled() { + return appLogsCollectionEnabled; + } + public boolean isReportHostName() { return reportHostName; } diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 23eeee9ba80..862df54aa1e 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -225,6 +225,14 @@ "aliases": [] } ], + "DD_APP_LOGS_COLLECTION_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": [] + } + ], "DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING": [ { "version": "C", From ba23ef3c7600e59ec6165f63c1f6a8e7fe3b3a10 Mon Sep 17 00:00:00 2001 From: jean-philippe bempel Date: Thu, 18 Dec 2025 16:19:28 +0100 Subject: [PATCH 2/2] Address comments --- .../log4j2/LoggerConfigInstrumentation.java | 2 +- .../logback/LogbackLoggerInstrumentation.java | 7 ++++--- .../main/java/datadog/trace/api/ConfigDefaults.java | 2 ++ .../src/main/java/datadog/trace/api/Config.java | 4 +++- .../java/datadog/trace/api/InstrumenterConfig.java | 11 +++++++++++ 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/LoggerConfigInstrumentation.java b/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/LoggerConfigInstrumentation.java index 264e54dd216..d5ec687d2e9 100644 --- a/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/LoggerConfigInstrumentation.java +++ b/dd-java-agent/instrumentation/log4j/log4j-2.0/src/main/java/datadog/trace/instrumentation/log4j2/LoggerConfigInstrumentation.java @@ -26,7 +26,7 @@ public LoggerConfigInstrumentation() { public boolean isApplicable(Set enabledSystems) { return (super.isApplicable(enabledSystems) && InstrumenterConfig.get().isAgentlessLogSubmissionEnabled()) - || Config.get().isAppLogsCollectionEnabled(); + || InstrumenterConfig.get().isAppLogsCollectionEnabled(); } @Override 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 b7ac7fb0228..aa4c5cdd540 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 @@ -19,6 +19,7 @@ import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.agent.tooling.InstrumenterModule; import datadog.trace.api.Config; +import datadog.trace.api.InstrumenterConfig; import datadog.trace.bootstrap.InstrumentationContext; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; @@ -64,7 +65,7 @@ public void methodAdvice(MethodTransformer transformer) { .and(takesArguments(1)) .and(takesArgument(0, named("ch.qos.logback.classic.spi.ILoggingEvent"))), LogbackLoggerInstrumentation.class.getName() + "$CallAppendersAdvice"); - if (Config.get().isAppLogsCollectionEnabled()) { + if (InstrumenterConfig.get().isAppLogsCollectionEnabled()) { transformer.applyAdvice( isMethod() .and(isPublic()) @@ -76,7 +77,7 @@ public void methodAdvice(MethodTransformer transformer) { } public static class CallAppendersAdvice { - @Advice.OnMethodEnter + @Advice.OnMethodEnter(suppress = Throwable.class) public static void onEnter(@Advice.Argument(0) ILoggingEvent event) { AgentSpan span = activeSpan(); @@ -88,7 +89,7 @@ public static void onEnter(@Advice.Argument(0) ILoggingEvent event) { } public static class CallAppendersAdvice2 { - @Advice.OnMethodEnter + @Advice.OnMethodEnter(suppress = Throwable.class) public static void onEnter(@Advice.Argument(0) ILoggingEvent event) { LogsIntakeHelper.log(event); } diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index 125258969a5..61b2ad73a77 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -122,6 +122,8 @@ public final class ConfigDefaults { // value to false if config is under breaking changes flag static final boolean DEFAULT_LOGS_INJECTION_ENABLED = true; + static final boolean DEFAULT_APP_LOGS_COLLECTION_ENABLED = false; + static final String DEFAULT_APPSEC_ENABLED = "inactive"; static final boolean DEFAULT_APPSEC_REPORTING_INBAND = false; static final int DEFAULT_APPSEC_TRACE_RATE_LIMIT = 100; diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index bcf75e25b2c..2461a5ba979 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -20,6 +20,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_APPSEC_TRACE_RATE_LIMIT; import static datadog.trace.api.ConfigDefaults.DEFAULT_APPSEC_WAF_METRICS; import static datadog.trace.api.ConfigDefaults.DEFAULT_APPSEC_WAF_TIMEOUT; +import static datadog.trace.api.ConfigDefaults.DEFAULT_APP_LOGS_COLLECTION_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_CASSANDRA_KEYSPACE_STATEMENT_EXTRACTION_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_CIVISIBILITY_AGENTLESS_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_CIVISIBILITY_AUTO_CONFIGURATION_ENABLED; @@ -1864,7 +1865,8 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins configProvider.getBoolean( LOGS_INJECTION_ENABLED, DEFAULT_LOGS_INJECTION_ENABLED, LOGS_INJECTION); } - appLogsCollectionEnabled = configProvider.getBoolean(APP_LOGS_COLLECTION_ENABLED, false); + appLogsCollectionEnabled = + configProvider.getBoolean(APP_LOGS_COLLECTION_ENABLED, DEFAULT_APP_LOGS_COLLECTION_ENABLED); dogStatsDNamedPipe = configProvider.getString(DOGSTATSD_NAMED_PIPE); diff --git a/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java b/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java index 293e9e0138c..fd3dcc25aaa 100644 --- a/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java +++ b/internal-api/src/main/java/datadog/trace/api/InstrumenterConfig.java @@ -2,6 +2,7 @@ import static datadog.trace.api.ConfigDefaults.DEFAULT_API_SECURITY_ENDPOINT_COLLECTION_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_APPSEC_ENABLED; +import static datadog.trace.api.ConfigDefaults.DEFAULT_APP_LOGS_COLLECTION_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_CIVISIBILITY_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_CODE_ORIGIN_FOR_SPANS_ENABLED; import static datadog.trace.api.ConfigDefaults.DEFAULT_DATA_JOBS_ENABLED; @@ -27,6 +28,7 @@ import static datadog.trace.api.config.AppSecConfig.APPSEC_ENABLED; import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_ENABLED; import static datadog.trace.api.config.GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED; +import static datadog.trace.api.config.GeneralConfig.APP_LOGS_COLLECTION_ENABLED; import static datadog.trace.api.config.GeneralConfig.DATA_JOBS_ENABLED; import static datadog.trace.api.config.GeneralConfig.INTERNAL_EXIT_ON_FAILURE; import static datadog.trace.api.config.GeneralConfig.TELEMETRY_ENABLED; @@ -206,6 +208,8 @@ public class InstrumenterConfig { private final boolean agentlessLogSubmissionEnabled; private final boolean apiSecurityEndpointCollectionEnabled; + private final boolean appLogsCollectionEnabled; + static { // Bind telemetry collector to config module before initializing ConfigProvider OtelEnvMetricCollectorProvider.register(OtelEnvMetricCollectorImpl.getInstance()); @@ -351,6 +355,9 @@ private InstrumenterConfig() { configProvider.getBoolean( API_SECURITY_ENDPOINT_COLLECTION_ENABLED, DEFAULT_API_SECURITY_ENDPOINT_COLLECTION_ENABLED); + + appLogsCollectionEnabled = + configProvider.getBoolean(APP_LOGS_COLLECTION_ENABLED, DEFAULT_APP_LOGS_COLLECTION_ENABLED); } public boolean isCodeOriginEnabled() { @@ -658,6 +665,10 @@ public boolean isApiSecurityEndpointCollectionEnabled() { return apiSecurityEndpointCollectionEnabled; } + public boolean isAppLogsCollectionEnabled() { + return appLogsCollectionEnabled; + } + // This has to be placed after all other static fields to give them a chance to initialize private static final InstrumenterConfig INSTANCE = new InstrumenterConfig(