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..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 @@ -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()) + || InstrumenterConfig.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..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 @@ -18,10 +18,13 @@ 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.api.InstrumenterConfig; 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 +32,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 +46,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,10 +65,19 @@ public void methodAdvice(MethodTransformer transformer) { .and(takesArguments(1)) .and(takesArgument(0, named("ch.qos.logback.classic.spi.ILoggingEvent"))), LogbackLoggerInstrumentation.class.getName() + "$CallAppendersAdvice"); + if (InstrumenterConfig.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 { - @Advice.OnMethodEnter + @Advice.OnMethodEnter(suppress = Throwable.class) public static void onEnter(@Advice.Argument(0) ILoggingEvent event) { AgentSpan span = activeSpan(); @@ -65,4 +87,11 @@ public static void onEnter(@Advice.Argument(0) ILoggingEvent event) { } } } + + public static class CallAppendersAdvice2 { + @Advice.OnMethodEnter(suppress = Throwable.class) + 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/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/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..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; @@ -349,6 +350,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 +887,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 +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, DEFAULT_APP_LOGS_COLLECTION_ENABLED); dogStatsDNamedPipe = configProvider.getString(DOGSTATSD_NAMED_PIPE); @@ -3511,6 +3516,10 @@ public boolean isLogsInjectionEnabled() { return logsInjectionEnabled; } + public boolean isAppLogsCollectionEnabled() { + return appLogsCollectionEnabled; + } + public boolean isReportHostName() { return reportHostName; } 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( 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",