Skip to content

Commit 36e924e

Browse files
Added GraphQL monitoring support (#6375)
* Added smoke-test SpringBoot GraphQL appication * Arguments capturing in GraphQL * Refactor * Fixed tests * Fixed tests * Fixed GraphQL resolver address * Missing smoke-test * Fixed tests
1 parent 76fe1a4 commit 36e924e

File tree

19 files changed

+471
-1
lines changed

19 files changed

+471
-1
lines changed

dd-java-agent/appsec/src/main/java/com/datadog/appsec/event/data/KnownAddresses.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ public interface KnownAddresses {
104104
// XXX: Not really used yet, but it's a known address and we should not treat it as unknown.
105105
Address<Object> GRAPHQL_SERVER_RESOLVER = new Address<>("graphql.server.resolver");
106106

107+
Address<Map<String, ?>> SERVER_GRAPHQL_ALL_RESOLVERS =
108+
new Address<>("server.graphql.all_resolvers");
109+
107110
Address<String> USER_ID = new Address<>("usr.id");
108111

109112
Address<Map<String, Object>> WAF_CONTEXT_PROCESSOR = new Address<>("waf.context.processor");
@@ -158,6 +161,8 @@ static Address<?> forName(String name) {
158161
return GRAPHQL_SERVER_ALL_RESOLVERS;
159162
case "graphql.server.resolver":
160163
return GRAPHQL_SERVER_RESOLVER;
164+
case "server.graphql.all_resolvers":
165+
return SERVER_GRAPHQL_ALL_RESOLVERS;
161166
case "usr.id":
162167
return USER_ID;
163168
case "waf.context.processor":

dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public class GatewayBridge {
7373
private volatile DataSubscriberInfo pathParamsSubInfo;
7474
private volatile DataSubscriberInfo respDataSubInfo;
7575
private volatile DataSubscriberInfo grpcServerRequestMsgSubInfo;
76+
private volatile DataSubscriberInfo graphqlServerRequestMsgSubInfo;
7677
private volatile DataSubscriberInfo requestEndSubInfo;
7778

7879
public GatewayBridge(
@@ -391,6 +392,33 @@ public void init() {
391392
}
392393
}
393394
});
395+
396+
subscriptionService.registerCallback(
397+
EVENTS.graphqlServerRequestMessage(),
398+
(RequestContext ctx_, Map<String, ?> data) -> {
399+
AppSecRequestContext ctx = ctx_.getData(RequestContextSlot.APPSEC);
400+
if (ctx == null) {
401+
return NoopFlow.INSTANCE;
402+
}
403+
while (true) {
404+
DataSubscriberInfo subInfo = graphqlServerRequestMsgSubInfo;
405+
if (subInfo == null) {
406+
subInfo =
407+
producerService.getDataSubscribers(KnownAddresses.GRAPHQL_SERVER_ALL_RESOLVERS);
408+
graphqlServerRequestMsgSubInfo = subInfo;
409+
}
410+
if (subInfo == null || subInfo.isEmpty()) {
411+
return NoopFlow.INSTANCE;
412+
}
413+
DataBundle bundle =
414+
new SingletonDataBundle<>(KnownAddresses.GRAPHQL_SERVER_ALL_RESOLVERS, data);
415+
try {
416+
return producerService.publishDataEvent(subInfo, ctx, bundle, true);
417+
} catch (ExpiredSubscriberInfoException e) {
418+
graphqlServerRequestMsgSubInfo = null;
419+
}
420+
}
421+
});
394422
}
395423

396424
public void stop() {

dd-java-agent/appsec/src/main/java/com/datadog/appsec/powerwaf/PowerWAFModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ private static Collection<Address<?>> getUsedAddresses(PowerwafContext ctx) {
384384
addressList.add(KnownAddresses.REQUEST_BODY_RAW);
385385
addressList.add(KnownAddresses.RESPONSE_HEADERS_NO_COOKIES);
386386
addressList.add(KnownAddresses.RESPONSE_BODY_OBJECT);
387+
addressList.add(KnownAddresses.GRAPHQL_SERVER_ALL_RESOLVERS);
387388

388389
return addressList;
389390
}

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/event/data/KnownAddressesSpecification.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class KnownAddressesSpecification extends Specification {
3939

4040
void 'number of known addresses is expected number'() {
4141
expect:
42-
Address.instanceCount() == 26
42+
Address.instanceCount() == 27
4343
KnownAddresses.WAF_CONTEXT_PROCESSOR.serial == Address.instanceCount() - 1
4444
}
4545
}

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class GatewayBridgeSpecification extends DDSpecification {
7777
TriConsumer<RequestContext, String, String> respHeaderCB
7878
Function<RequestContext, Flow<Void>> respHeadersDoneCB
7979
BiFunction<RequestContext, Object, Flow<Void>> grpcServerRequestMessageCB
80+
BiFunction<RequestContext, Map<String, Object>, Flow<Void>> graphqlServerRequestMessageCB
8081

8182
void setup() {
8283
callInitAndCaptureCBs()
@@ -410,6 +411,7 @@ class GatewayBridgeSpecification extends DDSpecification {
410411
1 * ig.registerCallback(EVENTS.responseHeader(), _) >> { respHeaderCB = it[1]; null }
411412
1 * ig.registerCallback(EVENTS.responseHeaderDone(), _) >> { respHeadersDoneCB = it[1]; null }
412413
1 * ig.registerCallback(EVENTS.grpcServerRequestMessage(), _) >> { grpcServerRequestMessageCB = it[1]; null }
414+
1 * ig.registerCallback(EVENTS.graphqlServerRequestMessage(), _) >> { graphqlServerRequestMessageCB = it[1]; null }
413415
0 * ig.registerCallback(_, _)
414416

415417
bridge.init()

dd-java-agent/instrumentation/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava/GraphQLDecorator.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
package datadog.trace.instrumentation.graphqljava;
22

3+
import static datadog.trace.api.gateway.Events.EVENTS;
4+
5+
import datadog.trace.api.gateway.CallbackProvider;
6+
import datadog.trace.api.gateway.Flow;
7+
import datadog.trace.api.gateway.RequestContext;
8+
import datadog.trace.api.gateway.RequestContextSlot;
39
import datadog.trace.api.naming.SpanNaming;
10+
import datadog.trace.bootstrap.ActiveSubsystems;
411
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
12+
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
513
import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes;
614
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
715
import datadog.trace.bootstrap.instrumentation.decorator.BaseDecorator;
16+
import graphql.execution.ExecutionContext;
17+
import graphql.language.Argument;
18+
import graphql.language.Field;
19+
import graphql.language.Selection;
20+
import graphql.language.StringValue;
21+
import graphql.language.Value;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
import java.util.function.BiFunction;
825

926
public class GraphQLDecorator extends BaseDecorator {
1027
public static final GraphQLDecorator DECORATE = new GraphQLDecorator();
@@ -16,6 +33,11 @@ public class GraphQLDecorator extends BaseDecorator {
1633
UTF8BytesString.create("graphql.validation");
1734
public static final CharSequence GRAPHQL_JAVA = UTF8BytesString.create("graphql-java");
1835

36+
// Extract this to allow for easier testing
37+
protected AgentTracer.TracerAPI tracer() {
38+
return AgentTracer.get();
39+
}
40+
1941
@Override
2042
protected String[] instrumentationNames() {
2143
return new String[] {"graphql-java"};
@@ -36,4 +58,52 @@ public AgentSpan afterStart(final AgentSpan span) {
3658
span.setMeasured(true);
3759
return super.afterStart(span);
3860
}
61+
62+
public AgentSpan onRequest(final AgentSpan span, final ExecutionContext context) {
63+
64+
if (ActiveSubsystems.APPSEC_ACTIVE) {
65+
66+
Map<String, Map<String, String>> resolversArgs = new HashMap<>();
67+
68+
for (Selection<?> selection :
69+
context.getOperationDefinition().getSelectionSet().getSelections()) {
70+
if (selection instanceof Field) {
71+
Field field = (Field) selection;
72+
String name = field.getName();
73+
74+
Map<String, String> arguments = new HashMap<>();
75+
76+
for (Argument argument : field.getArguments()) {
77+
String fieldName = argument.getName();
78+
Value<?> fieldValue = argument.getValue();
79+
if (fieldValue instanceof StringValue) {
80+
String stringValue = ((StringValue) fieldValue).getValue();
81+
arguments.put(fieldName, stringValue);
82+
}
83+
}
84+
resolversArgs.put(name, arguments);
85+
}
86+
}
87+
88+
CallbackProvider cbp = tracer().getCallbackProvider(RequestContextSlot.APPSEC);
89+
RequestContext ctx = span.getRequestContext();
90+
if (cbp == null || resolversArgs.isEmpty() || ctx == null) {
91+
return null;
92+
}
93+
94+
BiFunction<RequestContext, Map<String, ?>, Flow<Void>> graphqlResolverCallback =
95+
cbp.getCallback(EVENTS.graphqlServerRequestMessage());
96+
if (graphqlResolverCallback == null) {
97+
return null;
98+
}
99+
100+
Flow<Void> flow = graphqlResolverCallback.apply(ctx, resolversArgs);
101+
if (flow.getAction() instanceof Flow.Action.RequestBlockingAction) {
102+
// Blocking will be implemented in future PRs
103+
// span.setRequestBlockingAction((Flow.Action.RequestBlockingAction) flow.getAction());
104+
}
105+
}
106+
107+
return span;
108+
}
39109
}

dd-java-agent/instrumentation/graphql-java-14.0/src/main/java/datadog/trace/instrumentation/graphqljava/GraphQLInstrumentation.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public InstrumentationContext<ExecutionResult> beginExecuteOperation(
101101
requestSpan.setTag("graphql.operation.name", operationName);
102102
String resourceName = operationName != null ? operationName : state.getQuery();
103103
requestSpan.setResourceName(resourceName);
104+
DECORATE.onRequest(requestSpan, parameters.getExecutionContext());
104105
return SimpleInstrumentationContext.noOp();
105106
}
106107

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
plugins {
2+
id "com.github.johnrengelman.shadow"
3+
}
4+
5+
apply from: "$rootDir/gradle/java.gradle"
6+
description = 'SpringBoot GraphQL Smoke Tests.'
7+
8+
// The standard spring-boot plugin doesn't play nice with our project
9+
// so we'll build a fat jar instead
10+
jar {
11+
manifest {
12+
attributes('Main-Class': 'datadog.smoketest.appsec.springbootgraphql.SpringbootGraphqlApplication')
13+
}
14+
}
15+
16+
shadowJar {
17+
mergeServiceFiles {
18+
include 'META-INF/spring.*'
19+
}
20+
}
21+
22+
// Use Java 11 to build application
23+
tasks.withType(JavaCompile) {
24+
setJavaVersion(delegate, 11)
25+
}
26+
27+
dependencies {
28+
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.7.0'
29+
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-graphql', version: '2.7.0'
30+
implementation(group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.15.3')
31+
32+
testImplementation project(':dd-smoke-tests:appsec')
33+
}
34+
35+
tasks.withType(Test).configureEach {
36+
dependsOn "shadowJar"
37+
38+
jvmArgs "-Ddatadog.smoketest.appsec.springboot-graphql.shadowJar.path=${tasks.shadowJar.archiveFile.get()}"
39+
}
40+
41+
task testRuntimeActivation(type: Test) {
42+
jvmArgs '-Dsmoke_test.appsec.enabled=inactive',
43+
"-Ddatadog.smoketest.appsec.springboot-graphql.shadowJar.path=${tasks.shadowJar.archiveFile.get()}"
44+
}
45+
tasks['check'].dependsOn(testRuntimeActivation)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package datadog.smoketest.appsec.springbootgraphql;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class SpringbootGraphqlApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(SpringbootGraphqlApplication.class, args);
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package datadog.smoketest.appsec.springbootgraphql.controller;
2+
3+
import datadog.smoketest.appsec.springbootgraphql.dao.Author;
4+
import datadog.smoketest.appsec.springbootgraphql.dao.Book;
5+
import org.springframework.graphql.data.method.annotation.Argument;
6+
import org.springframework.graphql.data.method.annotation.QueryMapping;
7+
import org.springframework.graphql.data.method.annotation.SchemaMapping;
8+
import org.springframework.stereotype.Controller;
9+
10+
@Controller
11+
public class BookController {
12+
@QueryMapping
13+
public Book bookById(@Argument String id) {
14+
return Book.getById(id);
15+
}
16+
17+
@SchemaMapping
18+
public Author author(Book book) {
19+
return Author.getById(book.getAuthorId());
20+
}
21+
}

0 commit comments

Comments
 (0)