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 5c85b8a73ce..e2c7cf92006 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), + LOGS_CAPTURE(GeneralConfig.LOGS_CAPTURE_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 logsCaptureEnabled = 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); + logsCaptureEnabled = isFeatureEnabled(AgentFeature.LOGS_CAPTURE); llmObsEnabled = isFeatureEnabled(AgentFeature.LLMOBS); featureFlaggingEnabled = isFeatureEnabled(AgentFeature.FEATURE_FLAGGING); @@ -1122,15 +1126,16 @@ private static void maybeStartFeatureFlagging(final Class scoClass, final Obj } private static void maybeInstallLogsIntake(Class scoClass, Object sco) { - if (agentlessLogSubmissionEnabled) { + if (agentlessLogSubmissionEnabled || logsCaptureEnabled) { 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..3e60f37681d 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.isLogsCaptureEnabled()) { + 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..8b38335de9c 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 logsCaptureEnabled; + + public DatadogAppender(String name, Filter filter, Config config) { super(name, filter, null); + this.logsCaptureEnabled = config.isLogsCaptureEnabled(); } @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 (logsCaptureEnabled) { + // 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..a38657e4ec4 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().isLogsCaptureEnabled(); } @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/Log4jDatadogAppenderLogCaptureTest.groovy b/dd-java-agent/instrumentation/log4j/log4j-2.0/src/test/groovy/Log4jDatadogAppenderLogCaptureTest.groovy new file mode 100644 index 00000000000..f72b7baabb7 --- /dev/null +++ b/dd-java-agent/instrumentation/log4j/log4j-2.0/src/test/groovy/Log4jDatadogAppenderLogCaptureTest.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 Log4jDatadogAppenderLogCaptureTest extends InstrumentationSpecification { + + private static DummyLogsWriter logsWriter + + def setupSpec() { + logsWriter = new DummyLogsWriter() + LogsIntake.registerWriter(logsWriter) + } + + @Override + void configurePreAgent() { + super.configurePreAgent() + injectSysConfig(GeneralConfig.LOGS_CAPTURE_ENABLED, "true") + } + + def "test datadog appender registration"() { + setup: + ensureLog4jVersionCompatibleWithCurrentJVM() + + def logger = LogManager.getLogger(Log4jDatadogAppenderLogCaptureTest) + + 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") + "Log4jDatadogAppenderLogCaptureTest" == 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..5046dd2ebf6 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().isLogsCaptureEnabled(); + } + + @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().isLogsCaptureEnabled()) { + 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..f60a3fde526 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 logCapture = 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) } + logCapture = jarName.endsWith("LogCapture") + if (logCapture) { + jarName = jarName.substring(0, jarName.length() - "LogCapture".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 (supportsLogCapture()) { + command.add("-Ddd.$GeneralConfig.LOGS_CAPTURE_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 supportsLogCapture() { + 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 assertParsedJsonLinesWithLogCapture(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 (supportsLogCapture()) { + def lines = evpProxyMessages.collect { + it.v2 + }.flatten() as List> + assertParsedJsonLinesWithLogCapture(lines, firstTraceId, firstSpanId, secondTraceId, secondSpanId, thirdTraceId, thirdSpanId, forthTraceId, forthSpanId) + } } void checkTraceIdFormat(String traceId) { @@ -486,6 +599,18 @@ class Slf4jInterfaceLogbackBackend extends LogInjectionSmokeTest { } } +class Slf4jInterfaceLogbackBackendLogCapture extends Slf4jInterfaceLogbackBackend { + @Override + def supportsDirectLogSubmission() { + false + } + + @Override + def supportsLogCapture() { + 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 Slf4jInterfaceLog4j2BackendLogCapture extends Slf4jInterfaceLog4j2Backend { + @Override + def supportsDirectLogSubmission() { + false + } + + @Override + def supportsLogCapture() { + 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..40c9111b2e0 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 LOGS_CAPTURE_ENABLED = "logs.capture.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..0161641b547 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("logs_capture_enabled"); + writer.value(config.isLogsCaptureEnabled()); writer.endObject(); } 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..c6e77724be6 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -370,6 +370,7 @@ import static datadog.trace.api.config.GeneralConfig.HEALTH_METRICS_STATSD_PORT; import static datadog.trace.api.config.GeneralConfig.INSTRUMENTATION_SOURCE; import static datadog.trace.api.config.GeneralConfig.JDK_SOCKET_ENABLED; +import static datadog.trace.api.config.GeneralConfig.LOGS_CAPTURE_ENABLED; import static datadog.trace.api.config.GeneralConfig.LOG_LEVEL; import static datadog.trace.api.config.GeneralConfig.PERF_METRICS_ENABLED; import static datadog.trace.api.config.GeneralConfig.PRIMARY_TAG; @@ -885,6 +886,7 @@ public static String getHostName() { private final boolean traceInferredProxyEnabled; private final int clockSyncPeriod; private final boolean logsInjectionEnabled; + private final boolean logsCaptureEnabled; 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); } + logsCaptureEnabled = configProvider.getBoolean(LOGS_CAPTURE_ENABLED, false); dogStatsDNamedPipe = configProvider.getString(DOGSTATSD_NAMED_PIPE); @@ -3511,6 +3514,10 @@ public boolean isLogsInjectionEnabled() { return logsInjectionEnabled; } + public boolean isLogsCaptureEnabled() { + return logsCaptureEnabled; + } + public boolean isReportHostName() { return reportHostName; } diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 3274a255515..58c64d1cdd0 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -273,6 +273,7 @@ "DD_LLMOBS_ML_APP": ["A"], "DD_LOGS_INJECTION": ["A"], "DD_LOGS_INJECTION_ENABLED": ["A"], + "DD_LOGS_CAPTURE_ENABLED": ["A"], "DD_LOG_LEVEL": ["A"], "DD_MEASURE_METHODS": ["A"], "DD_MESSAGE_BROKER_SPLIT_BY_DESTINATION": ["A"],