diff --git a/dd-java-agent/agent-bootstrap/build.gradle b/dd-java-agent/agent-bootstrap/build.gradle index e8c8d4147b8..3aa57b554e2 100644 --- a/dd-java-agent/agent-bootstrap/build.gradle +++ b/dd-java-agent/agent-bootstrap/build.gradle @@ -21,6 +21,7 @@ dependencies { api project(':internal-api') api project(':internal-api:internal-api-9') api project(':dd-java-agent:agent-logging') + api project(':dd-java-agent:agent-debugger:debugger-bootstrap') api libs.slf4j // ^ Generally a bad idea for libraries, but we're shadowing. diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturingTestBase.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturingTestBase.java index 991bdee2b2e..92fb5c57b41 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturingTestBase.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturingTestBase.java @@ -351,6 +351,7 @@ public static LogProbe.Builder createProbeBuilder( protected TestSnapshotListener installProbes( Configuration configuration, ProbeDefinition... probes) { + config = mock(Config.class); when(config.isDebuggerEnabled()).thenReturn(true); when(config.isDebuggerClassFileDumpEnabled()).thenReturn(true); diff --git a/dd-java-agent/instrumentation/grpc-1.5/build.gradle b/dd-java-agent/instrumentation/grpc-1.5/build.gradle index c32c11f72f1..8c0db536c82 100644 --- a/dd-java-agent/instrumentation/grpc-1.5/build.gradle +++ b/dd-java-agent/instrumentation/grpc-1.5/build.gradle @@ -39,6 +39,8 @@ dependencies { testImplementation group: 'io.grpc', name: 'grpc-protobuf', version: grpcVersion testImplementation group: 'io.grpc', name: 'grpc-stub', version: grpcVersion testImplementation group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2' + testImplementation project(':dd-java-agent:agent-debugger') + testImplementation libs.bundles.mockito latestDepTestImplementation sourceSets.test.output // include the protobuf generated classes latestDepTestCompileOnly group: 'io.grpc', name: 'grpc-core', version: '1.+' diff --git a/dd-java-agent/instrumentation/grpc-1.5/src/main/java/datadog/trace/instrumentation/grpc/server/MethodHandlersInstrumentation.java b/dd-java-agent/instrumentation/grpc-1.5/src/main/java/datadog/trace/instrumentation/grpc/server/MethodHandlersInstrumentation.java new file mode 100644 index 00000000000..6884862719b --- /dev/null +++ b/dd-java-agent/instrumentation/grpc-1.5/src/main/java/datadog/trace/instrumentation/grpc/server/MethodHandlersInstrumentation.java @@ -0,0 +1,60 @@ +package datadog.trace.instrumentation.grpc.server; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.nameEndsWith; +import static datadog.trace.bootstrap.debugger.spanorigin.CodeOriginInfo.entry; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter.ForTypeHierarchy; +import datadog.trace.agent.tooling.InstrumenterModule; +import java.lang.reflect.Method; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumenterModule.class) +public class MethodHandlersInstrumentation extends InstrumenterModule.Tracing + implements ForTypeHierarchy { + private static final ElementMatcher METHOD_HANDLERS = + nameEndsWith("$MethodHandlers"); + + public MethodHandlersInstrumentation() { + super("grpc-server-code-origin"); + } + + @Override + public String hierarchyMarkerType() { + return "io.grpc.MethodDescriptor"; + } + + @Override + public ElementMatcher hierarchyMatcher() { + return METHOD_HANDLERS; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isConstructor().and(takesArguments(2)), + MethodHandlersInstrumentation.class.getName() + "$BuildAdvice"); + } + + public static class BuildAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter(@Advice.Argument(0) Object serviceImpl) { + Class serviceClass = serviceImpl.getClass(); + Class superclass = serviceClass.getSuperclass(); + if (superclass != null) { + for (Method method : superclass.getDeclaredMethods()) { + try { + entry(serviceClass.getDeclaredMethod(method.getName(), method.getParameterTypes())); + } catch (NoSuchMethodException e) { + // service method not overridden on the impl. skipping instrumentation. + } + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/grpc-1.5/src/test/groovy/GrpcCodeOriginTest.groovy b/dd-java-agent/instrumentation/grpc-1.5/src/test/groovy/GrpcCodeOriginTest.groovy new file mode 100644 index 00000000000..fb1204ae45e --- /dev/null +++ b/dd-java-agent/instrumentation/grpc-1.5/src/test/groovy/GrpcCodeOriginTest.groovy @@ -0,0 +1,324 @@ +import com.datadog.debugger.agent.ClassesToRetransformFinder +import com.datadog.debugger.agent.Configuration +import com.datadog.debugger.agent.ConfigurationUpdater +import com.datadog.debugger.agent.DebuggerTransformer +import com.datadog.debugger.agent.DenyListHelper +import com.datadog.debugger.agent.JsonSnapshotSerializer +import com.datadog.debugger.codeorigin.DefaultCodeOriginRecorder +import com.datadog.debugger.instrumentation.InstrumentationResult +import com.datadog.debugger.probe.ProbeDefinition +import com.datadog.debugger.sink.DebuggerSink +import com.datadog.debugger.sink.ProbeStatusSink +import com.google.common.util.concurrent.MoreExecutors +import datadog.trace.agent.test.naming.VersionedNamingTestBase +import datadog.trace.api.Config +import datadog.trace.api.DDSpanTypes +import datadog.trace.api.DDTags +import datadog.trace.bootstrap.debugger.DebuggerContext +import datadog.trace.bootstrap.instrumentation.api.Tags +import datadog.trace.util.AgentTaskScheduler +import example.GreeterGrpc +import example.Helloworld +import io.grpc.BindableService +import io.grpc.ManagedChannel +import io.grpc.Server +import io.grpc.inprocess.InProcessChannelBuilder +import io.grpc.inprocess.InProcessServerBuilder +import io.grpc.stub.StreamObserver +import net.bytebuddy.agent.ByteBuddyAgent + +import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference + +import static datadog.trace.api.config.TraceInstrumentationConfig.* +import static datadog.trace.util.AgentThreadFactory.AgentThread.TASK_SCHEDULER +import static java.lang.String.format +import static org.mockito.Mockito.mock +import static org.mockito.Mockito.when + +abstract class GrpcCodeOriginTest extends VersionedNamingTestBase { + + @Override + final String service() { + return null + } + + @Override + final String operation() { + return null + } + + protected abstract String clientOperation() + + protected abstract String serverOperation() + + protected boolean hasClientMessageSpans() { + false + } + + @Override + protected void configurePreAgent() { + super.configurePreAgent() + injectSysConfig("dd.trace.grpc.ignored.inbound.methods", "example.Greeter/IgnoreInbound") + injectSysConfig("dd.trace.grpc.ignored.outbound.methods", "example.Greeter/Ignore") + if (hasClientMessageSpans()) { + injectSysConfig("integration.grpc-message.enabled", "true") + } + // here to trigger wrapping to record scheduling time - the logic is trivial so it's enough to verify + // that ClassCastExceptions do not arise from the wrapping + injectSysConfig("dd.profiling.enabled", "true") + codeOriginSetup() + } + + def "test conversation #name"() { + setup: + + def msgCount = serverMessageCount + def serverReceived = new CopyOnWriteArrayList<>() + def clientReceived = new CopyOnWriteArrayList<>() + def error = new AtomicReference() + + BindableService greeter = new GreeterGrpc.GreeterImplBase() { + @Override + StreamObserver conversation(StreamObserver observer) { + return new StreamObserver() { + @Override + void onNext(Helloworld.Response value) { + + serverReceived << value.message + + (1..msgCount).each { + if (TEST_TRACER.activeScope().isAsyncPropagating()) { + observer.onNext(value) + } else { + observer.onError(new IllegalStateException("not async propagating!")) + } + } + } + + @Override + void onError(Throwable t) { + if (TEST_TRACER.activeScope().isAsyncPropagating()) { + error.set(t) + observer.onError(t) + } else { + observer.onError(new IllegalStateException("not async propagating!")) + } + } + + @Override + void onCompleted() { + if (TEST_TRACER.activeScope().isAsyncPropagating()) { + observer.onCompleted() + } else { + observer.onError(new IllegalStateException("not async propagating!")) + } + } + } + } + } + Server server = InProcessServerBuilder.forName(getClass().name).addService(greeter) + .executor(directExecutor ? MoreExecutors.directExecutor() : Executors.newCachedThreadPool()) + .build().start() + + Thread.sleep(1000) + ManagedChannel channel = InProcessChannelBuilder.forName(getClass().name).build() + GreeterGrpc.GreeterStub client = GreeterGrpc.newStub(channel).withWaitForReady() + + when: + def streamObserver = client.conversation(new StreamObserver() { + @Override + void onNext(Helloworld.Response value) { + if (TEST_TRACER.activeScope().isAsyncPropagating()) { + clientReceived << value.message + } else { + error.set(new IllegalStateException("not async propagating!")) + } + } + + @Override + void onError(Throwable t) { + if (TEST_TRACER.activeScope().isAsyncPropagating()) { + error.set(t) + } else { + error.set(new IllegalStateException("not async propagating!")) + } + } + + @Override + void onCompleted() { + if (!TEST_TRACER.activeScope().isAsyncPropagating()) { + error.set(new IllegalStateException("not async propagating!")) + } + } + }) + + clientRange.each { + def message = Helloworld.Response.newBuilder().setMessage("call $it").build() + streamObserver.onNext(message) + } + streamObserver.onCompleted() + + then: + error.get() == null + TEST_WRITER.waitForTraces(2) + error.get() == null + serverReceived == clientRange.collect { "call $it" } + clientReceived == serverRange.collect { + clientRange.collect { + "call $it" + } + }.flatten().sort() + + assertTraces(2) { + trace((hasClientMessageSpans() ? clientMessageCount * serverMessageCount : 0) + 1) { + span { + operationName clientOperation() + resourceName "example.Greeter/Conversation" + spanType DDSpanTypes.RPC + parent() + errored false + tags { + "$Tags.COMPONENT" "grpc-client" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.RPC_SERVICE" "example.Greeter" + "status.code" "OK" + "request.type" "example.Helloworld\$Response" + "response.type" "example.Helloworld\$Response" + peerServiceFrom(Tags.RPC_SERVICE) + defaultTags() + } + } + if (hasClientMessageSpans()) { + (1..(clientMessageCount * serverMessageCount)).each { + span { + operationName "grpc.message" + resourceName "grpc.message" + spanType DDSpanTypes.RPC + childOf span(0) + errored false + tags { + "$Tags.COMPONENT" "grpc-client" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "message.type" "example.Helloworld\$Response" + defaultTagsNoPeerService() + } + } + } + } + } + trace(clientMessageCount + 1) { + span { + operationName serverOperation() + resourceName "example.Greeter/Conversation" + spanType DDSpanTypes.RPC + childOf trace(0).get(0) + errored false + tags { + "$Tags.COMPONENT" "grpc-server" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER + "status.code" "OK" + isPresent(DDTags.DD_CODE_ORIGIN_TYPE) + isPresent(format(DDTags.DD_CODE_ORIGIN_FRAME, 0, "signature")) + + for (i in 0..<8) { + for (label in ["file", "line", "method", "type"]) { + isPresent(format(DDTags.DD_CODE_ORIGIN_FRAME, i, label)) + } + } + defaultTags(true) + } + } + clientRange.each { + span { + operationName "grpc.message" + resourceName "grpc.message" + spanType DDSpanTypes.RPC + childOf span(0) + errored false + tags { + "$Tags.COMPONENT" "grpc-server" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER + "message.type" "example.Helloworld\$Response" + defaultTags() + } + } + } + } + } + + cleanup: + channel?.shutdownNow()?.awaitTermination(10, TimeUnit.SECONDS) + server?.shutdownNow()?.awaitTermination() + + where: + name | clientMessageCount | serverMessageCount | directExecutor + "A" | 1 | 1 | false + "B" | 2 | 1 | false + "C" | 1 | 2 | false + "D" | 2 | 2 | false + "E" | 3 | 3 | false + "A" | 1 | 1 | true + "B" | 2 | 1 | true + "C" | 1 | 2 | true + "D" | 2 | 2 | true + "E" | 3 | 3 | true + + clientRange = 1..clientMessageCount + serverRange = 1..serverMessageCount + } + + + void codeOriginSetup() { + injectSysConfig(CODE_ORIGIN_FOR_SPANS_ENABLED, "true", true) + + def configuration = Configuration.builder() + .setService("grpc code origin test") + .build() + + def config = mock(Config.class) + when(config.isDebuggerEnabled()).thenReturn(true) + when(config.isDebuggerClassFileDumpEnabled()).thenReturn(true) + when(config.isDebuggerVerifyByteCode()).thenReturn(false) + when(config.getFinalDebuggerSnapshotUrl()) + .thenReturn("http://localhost:8126/debugger/v1/input") + when(config.getFinalDebuggerSymDBUrl()).thenReturn("http://localhost:8126/symdb/v1/input") + when(config.getDebuggerCodeOriginMaxUserFrames()).thenReturn(8) + + def probeStatusSink = mock(ProbeStatusSink.class) + + def sink = new DebuggerSink(config, probeStatusSink) + def configurationUpdater = new ConfigurationUpdater(INSTRUMENTATION, DebuggerTransformer::new, config, sink, new ClassesToRetransformFinder()) + + def currentTransformer = new DebuggerTransformer(config, configuration, { + ProbeDefinition definition, InstrumentationResult result -> + }, sink) + INSTRUMENTATION.addTransformer(currentTransformer) + + DebuggerContext.initProbeResolver(configurationUpdater) + DebuggerContext.initClassFilter(new DenyListHelper(null)) + DebuggerContext.initValueSerializer(new JsonSnapshotSerializer()) + + DebuggerContext.initCodeOrigin(new DefaultCodeOriginRecorder(config, configurationUpdater)) + } +} + +class GrpcCodeOriginForkedTest extends GrpcCodeOriginTest { + + @Override + int version() { + return 1 + } + + @Override + protected String clientOperation() { + return "grpc.client.request" + } + + @Override + protected String serverOperation() { + return "grpc.server.request" + } +} diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy index 04a5d124a3b..33b8233af24 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/asserts/TagsAssert.groovy @@ -49,6 +49,11 @@ class TagsAssert { defaultTags(distributedRootSpan, false) } + def isPresent(String name) { + tag(name, { it != null }) + } + + /** * @param distributedRootSpan set to true if current span has a parent span but still considered 'root' for current service */