Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ abstract class GraphQLTest extends VersionedNamingTestBase {
.type(newTypeWiring("Book").dataFetcher("year", new DataFetcher<CompletionStage<Integer>>() {
@Override
CompletionStage<Integer> get(DataFetchingEnvironment environment) throws Exception {
return CompletableFuture.completedStage(2015)
return CompletableFuture.completedFuture(2015)
}
}))
.build()
Expand Down Expand Up @@ -359,6 +359,23 @@ abstract class GraphQLTest extends VersionedNamingTestBase {
"graphql.operation.name" null
"error.message" { it.contains("Field 'title' in type 'Book' is undefined") }
"error.message" { it.contains("(and 1 more errors)") }
"events" {
def events = new groovy.json.JsonSlurper().parseText(it) as List
events.size() == 2
def event1 = events[0]
event1.name == "dd.graphql.query.error"
event1.time_unix_nano instanceof Long
def attrs1 = event1.attributes
attrs1.message == "Validation error of type FieldUndefined: Field 'title' in type 'Book' is undefined @ 'bookById/title'"
attrs1.locations == ["4:5"]

def event2 = events[1]
event2.name == "dd.graphql.query.error"
event2.time_unix_nano instanceof Long
def attrs2 = event2.attributes
attrs2.message == "Validation error of type FieldUndefined: Field 'color' in type 'Book' is undefined @ 'bookById/color'"
attrs2.locations == ["5:5"]
}
defaultTags()
}
}
Expand Down Expand Up @@ -417,6 +434,16 @@ abstract class GraphQLTest extends VersionedNamingTestBase {
"graphql.source" query
"graphql.operation.name" null
"error.message" { it.toLowerCase().startsWith("invalid syntax") }
"events" {
def events = new groovy.json.JsonSlurper().parseText(it) as List
events.size() == 1
def event = events[0]
event.name == "dd.graphql.query.error"
event.time_unix_nano instanceof Long
def attrs = event.attributes
attrs.message == "Invalid Syntax : offending token ')' at line 2 column 25"
attrs.locations == ["2:25"]
}
defaultTags()
}
}
Expand Down Expand Up @@ -472,6 +499,17 @@ abstract class GraphQLTest extends VersionedNamingTestBase {
"graphql.source" expectedQuery
"graphql.operation.name" "findBookById"
"error.message" "Exception while fetching data (/bookById/cover) : TEST"
"events" {
def events = new groovy.json.JsonSlurper().parseText(it) as List
events.size() == 1
def event = events[0]
event.name == "dd.graphql.query.error"
event.time_unix_nano instanceof Long
def attrs = event.attributes
attrs.message == "Exception while fetching data (/bookById/cover) : TEST"
attrs.locations == ["4:5"]
attrs.path == ["bookById", "cover"]
}
defaultTags()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static datadog.trace.instrumentation.graphqljava.GraphQLDecorator.DECORATE;

import datadog.trace.api.Config;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import graphql.ExecutionResult;
import graphql.GraphQLError;
Expand All @@ -10,6 +11,7 @@

public class ExecutionInstrumentationContext extends SimpleInstrumentationContext<ExecutionResult> {
private final State state;
private static final List<String> errorExtensions = Config.get().getTraceGraphqlErrorExtensions();

public ExecutionInstrumentationContext(State state) {
this.state = state;
Expand All @@ -30,6 +32,11 @@ public void onCompleted(ExecutionResult result, Throwable t) {
}
requestSpan.setErrorMessage(error);
requestSpan.setError(true);

// Add span events for each GraphQL error
for (GraphQLError graphQLError : errors) {
DECORATE.errorSpanEvent(requestSpan, graphQLError);
}
}
}
requestSpan.setTag("graphql.source", state.getQuery());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static datadog.trace.api.gateway.Events.EVENTS;

import datadog.trace.api.Config;
import datadog.trace.api.gateway.CallbackProvider;
import datadog.trace.api.gateway.Flow;
import datadog.trace.api.gateway.RequestContext;
Expand All @@ -11,17 +12,21 @@
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes;
import datadog.trace.bootstrap.instrumentation.api.SpanNativeAttributes;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
import datadog.trace.bootstrap.instrumentation.decorator.BaseDecorator;
import graphql.GraphQLError;
import graphql.execution.ExecutionContext;
import graphql.language.Argument;
import graphql.language.Field;
import graphql.language.Selection;
import graphql.language.StringValue;
import graphql.language.Value;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

public class GraphQLDecorator extends BaseDecorator {
public static final GraphQLDecorator DECORATE = new GraphQLDecorator();
Expand All @@ -32,6 +37,7 @@ public class GraphQLDecorator extends BaseDecorator {
public static final CharSequence GRAPHQL_VALIDATION =
UTF8BytesString.create("graphql.validation");
public static final CharSequence GRAPHQL_JAVA = UTF8BytesString.create("graphql-java");
private static final List<String> errorExtensions = Config.get().getTraceGraphqlErrorExtensions();

// Extract this to allow for easier testing
protected AgentTracer.TracerAPI tracer() {
Expand Down Expand Up @@ -106,4 +112,64 @@ public AgentSpan onRequest(final AgentSpan span, final ExecutionContext context)

return span;
}

public AgentSpan errorSpanEvent(AgentSpan requestSpan, GraphQLError graphQLError) {
SpanNativeAttributes.Builder attributes =
SpanNativeAttributes.builder().put("message", graphQLError.getMessage());

// Add locations if available
if (graphQLError.getLocations() != null && !graphQLError.getLocations().isEmpty()) {
List<String> locationStrings =
graphQLError.getLocations().stream()
.map(loc -> loc.getLine() + ":" + loc.getColumn())
.collect(Collectors.toList());
attributes.putStringArray("locations", locationStrings);
}

// Add path if available
if (graphQLError.getPath() != null && !graphQLError.getPath().isEmpty()) {
List<String> pathStrings =
graphQLError.getPath().stream().map(Object::toString).collect(Collectors.toList());
attributes.putStringArray("path", pathStrings);
}

// Add extensions if available
Map<String, Object> extensions = graphQLError.getExtensions();
if (extensions != null && !extensions.isEmpty()) {

for (String extensionKey : errorExtensions) {
if (extensions.containsKey(extensionKey)) {
Object value = extensions.get(extensionKey);
if (value != null) {
if (value instanceof Number) {
if (value instanceof Long) {
attributes.put("extensions." + extensionKey, (Long) value);
} else if (value instanceof Double) {
attributes.put("extensions." + extensionKey, (Double) value);
} else {
attributes.put("extensions." + extensionKey, value.toString());
}
} else if (value instanceof Boolean) {
attributes.put("extensions." + extensionKey, (Boolean) value);
} else if (value instanceof List) {
List<?> list = (List<?>) value;
if (!list.isEmpty() && list.get(0) instanceof String) {
attributes.putStringArray(
"extensions." + extensionKey,
list.stream().map(Object::toString).collect(Collectors.toList()));
} else {
attributes.put("extensions." + extensionKey, value.toString());
}
} else {
attributes.put("extensions." + extensionKey, value.toString());
}
}
}
}
}

requestSpan.addEvent("dd.graphql.query.error", attributes.build());

return requestSpan;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -342,5 +342,7 @@ public final class ConfigDefaults {
"$.Credentials.SessionToken",
"$.InventoryConfigurationList[*].Destination.S3BucketDestination.Encryption.SSEKMS.KeyId");

static final String DEFAULT_TRACE_GRAPHQL_ERROR_EXTENSIONS = " , , ,";

private ConfigDefaults() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,5 +176,7 @@ public final class TraceInstrumentationConfig {
public static final String SQS_BODY_PROPAGATION_ENABLED = "trace.sqs.body.propagation.enabled";
public static final String ADD_SPAN_POINTERS = "add.span.pointers";

public static final String TRACE_GRAPHQL_ERROR_EXTENSIONS = "trace.graphql.error.extensions";

private TraceInstrumentationConfig() {}
}
30 changes: 29 additions & 1 deletion dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
import datadog.trace.bootstrap.instrumentation.api.AttachableWrapper;
import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities;
import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities;
import datadog.trace.bootstrap.instrumentation.api.SpanNativeAttributes;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import javax.annotation.Nonnull;
Expand Down Expand Up @@ -109,6 +111,8 @@ static DDSpan create(

protected final List<AgentSpanLink> links;

private final List<DDSpanEvent> events;

/**
* Spans should be constructed using the builder, not by calling the constructor directly.
*
Expand Down Expand Up @@ -136,6 +140,7 @@ private DDSpan(
}

this.links = links == null ? new CopyOnWriteArrayList<>() : new CopyOnWriteArrayList<>(links);
this.events = new CopyOnWriteArrayList<>();
}

public boolean isFinished() {
Expand Down Expand Up @@ -704,7 +709,7 @@ public CharSequence getType() {

@Override
public void processTagsAndBaggage(final MetadataConsumer consumer) {
context.processTagsAndBaggage(consumer, longRunningVersion, links);
context.processTagsAndBaggage(consumer, longRunningVersion, links, events);
}

@Override
Expand Down Expand Up @@ -856,4 +861,27 @@ public boolean isOutbound() {
Object spanKind = context.getTag(Tags.SPAN_KIND);
return Tags.SPAN_KIND_CLIENT.equals(spanKind) || Tags.SPAN_KIND_PRODUCER.equals(spanKind);
}

public AgentSpan addEvent(String name) {
return addEvent(name, null);
}

public AgentSpan addEvent(String name, SpanNativeAttributes attributes) {
if (name != null) {
events.add(new DDSpanEvent(name, attributes));
}
return this;
}

public AgentSpan addEvent(
String name, SpanNativeAttributes attributes, long timestamp, TimeUnit unit) {
if (name != null) {
events.add(new DDSpanEvent(name, attributes, unit.toNanos(timestamp)));
}
return this;
}

public List<DDSpanEvent> getEvents() {
return events;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package datadog.trace.core;

import static datadog.trace.api.DDTags.SPAN_EVENTS;
import static datadog.trace.api.DDTags.SPAN_LINKS;
import static datadog.trace.api.cache.RadixTreeCache.HTTP_STATUSES;
import static datadog.trace.bootstrap.instrumentation.api.ErrorPriorities.UNSET;
Expand Down Expand Up @@ -845,7 +846,10 @@ public <T> void setMetaStruct(final String field, final T value) {
}

public void processTagsAndBaggage(
final MetadataConsumer consumer, int longRunningVersion, List<AgentSpanLink> links) {
final MetadataConsumer consumer,
int longRunningVersion,
List<AgentSpanLink> links,
List<DDSpanEvent> events) {
synchronized (unsafeTags) {
// Tags
Map<String, Object> tags =
Expand All @@ -854,6 +858,10 @@ public void processTagsAndBaggage(
if (linksTag != null) {
tags.put(SPAN_LINKS, linksTag);
}
String eventsTag = DDSpanEvent.toTag(events);
if (events != null && !events.isEmpty()) {
tags.put(SPAN_EVENTS, eventsTag);
}
// Baggage
Map<String, String> baggageItemsWithPropagationTags;
if (injectBaggageAsTags) {
Expand Down
Loading