Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL java instrumentation #5583

Merged
merged 11 commits into from
Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/standalone-library-instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ that can be used if you prefer that over using the Java agent:
* [AWS SDK 2.2+](../instrumentation/aws-sdk/aws-sdk-2.2/library)
* [gRPC](../instrumentation/grpc-1.6/library)
* [Guava](../instrumentation/guava-10.0/library)
* [GraphQL Java](../instrumentation/graphql-java-12.0/library)
* [JDBC](../instrumentation/jdbc/library)
* [Lettuce](../instrumentation/lettuce/lettuce-5.1/library)
* [Log4j appender](../instrumentation/log4j/log4j-appender-2.16/library)
Expand Down
2 changes: 1 addition & 1 deletion docs/supported-libraries.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Supported libraries, frameworks, application servers, and JVMs

We automatically instrument and support a huge number of libraries, frameworks,
Expand Down Expand Up @@ -57,6 +56,7 @@ These are the supported libraries and frameworks:
| [Geode Client](https://geode.apache.org/) | 1.4+ |
| [Google HTTP Client](https://github.com/googleapis/google-http-java-client) | 1.19+ |
| [Grails](https://grails.org/) | 3.0+ |
| [GraphQL Java](https://www.graphql-java.com/) | 12.0+ |
| [gRPC](https://github.com/grpc/grpc-java) | 1.6+ |
| [Guava ListenableFuture](https://guava.dev/releases/snapshot/api/docs/com/google/common/util/concurrent/ListenableFuture.html) | 10.0+ |
| [GWT](http://www.gwtproject.org/) | 2.0+ |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ class AcceptableVersions(private val skipVersions: Collection<String>) :
|| versionString.contains("-atlassian-")
|| versionString.contains("public_draft")
|| versionString.contains("snapshot")
|| versionString.contains("test")
|| GIT_SHA_PATTERN.matches(versionString)
|| DATETIME_PATTERN.matches(versionString)
return !draftVersion
}

companion object {
private val GIT_SHA_PATTERN = Regex("^.*-[0-9a-f]{7,}$")
private val DATETIME_PATTERN = Regex("^\\d{4}-\\d{2}-\\d{2}t\\d{2}-\\d{2}-\\d{2}.*$")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

graphql-java has a ton of weird versions https://repo1.maven.org/maven2/com/graphql-java/graphql-java/

}
}
6 changes: 6 additions & 0 deletions instrumentation/graphql-java-12.0/javaagent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Settings for the GraphQL instrumentation

| System property | Type | Default | Description |
|---|---|---------|--------------------------------------------------------------------------------------------|
| `otel.instrumentation.graphql.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. |
| `otel.instrumentation.graphql.query-sanitizer.enabled` | Boolean | `true` | Whether to remove sensitive information from query source that is added as span attribute. |
25 changes: 25 additions & 0 deletions instrumentation/graphql-java-12.0/javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
id("otel.javaagent-instrumentation")
}

muzzle {
pass {
group.set("com.graphql-java")
module.set("graphql-java")
versions.set("[12,)")
skip("230521-nf-execution")
assertInverse.set(true)
}
}

dependencies {
implementation(project(":instrumentation:graphql-java-12.0:library"))

library("com.graphql-java:graphql-java:12.0")

testImplementation(project(":instrumentation:graphql-java-12.0:testing"))
}

tasks.withType<Test>().configureEach {
jvmArgs("-Dotel.instrumentation.graphql.experimental-span-attributes=true")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.graphql;

import static io.opentelemetry.javaagent.instrumentation.graphql.GraphqlSingletons.addInstrumentation;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
import static net.bytebuddy.matcher.ElementMatchers.returns;

import graphql.execution.instrumentation.Instrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

public class GraphqlInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("graphql.GraphQL");
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
namedOneOf("checkInstrumentationDefaultState", "checkInstrumentation")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These methods add default instrumentations. By applying our instrumentation after these our instrumentation will end up surrounding all other instrumentation.

.and(returns(named("graphql.execution.instrumentation.Instrumentation"))),
this.getClass().getName() + "$AddInstrumentationAdvice");
}

@SuppressWarnings("unused")
public static class AddInstrumentationAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(@Advice.Return(readOnly = false) Instrumentation instrumentation) {
instrumentation = addInstrumentation(instrumentation);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.graphql;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.Collections;
import java.util.List;

@SuppressWarnings("unused")
@AutoService(InstrumentationModule.class)
public class GraphqlInstrumentationModule extends InstrumentationModule {

public GraphqlInstrumentationModule() {
super("graphql-java", "graphql-java-12.0");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return Collections.singletonList(new GraphqlInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.graphql;

import graphql.execution.instrumentation.ChainedInstrumentation;
import graphql.execution.instrumentation.Instrumentation;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.api.config.Config;
import io.opentelemetry.instrumentation.graphql.GraphQLTracing;
import io.opentelemetry.instrumentation.graphql.OpenTelemetryInstrumentation;
import java.util.ArrayList;
import java.util.List;

public final class GraphqlSingletons {

private static final boolean QUERY_SANITIZATION_ENABLED =
Config.get().getBoolean("otel.instrumentation.graphql.query-sanitizer.enabled", true);
private static final boolean CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES =
Config.get().getBoolean("otel.instrumentation.graphql.experimental-span-attributes", false);

private static final GraphQLTracing TRACING =
GraphQLTracing.builder(GlobalOpenTelemetry.get())
.setCaptureExperimentalSpanAttributes(CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES)
.setSanitizeQuery(QUERY_SANITIZATION_ENABLED)
.build();

private GraphqlSingletons() {}

public static Instrumentation addInstrumentation(Instrumentation instrumentation) {
Instrumentation ourInstrumentation = TRACING.newInstrumentation();
if (instrumentation == null) {
return ourInstrumentation;
}
if (instrumentation instanceof OpenTelemetryInstrumentation) {
return instrumentation;
}
List<Instrumentation> instrumentationList = new ArrayList<>();
if (instrumentation instanceof ChainedInstrumentation) {
instrumentationList.addAll(((ChainedInstrumentation) instrumentation).getInstrumentations());
} else {
instrumentationList.add(instrumentation);
}
boolean containsOurInstrumentation =
instrumentationList.stream()
.anyMatch(instr -> instr instanceof OpenTelemetryInstrumentation);
laurit marked this conversation as resolved.
Show resolved Hide resolved
if (!containsOurInstrumentation) {
instrumentationList.add(0, ourInstrumentation);
}
return new ChainedInstrumentation(instrumentationList);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.graphql;

import graphql.GraphQL;
import io.opentelemetry.instrumentation.graphql.AbstractGraphqlTest;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import org.junit.jupiter.api.extension.RegisterExtension;

public class GraphqlTest extends AbstractGraphqlTest {

@RegisterExtension
private static final InstrumentationExtension testing = AgentInstrumentationExtension.create();

@Override
protected InstrumentationExtension getTesting() {
return testing;
}

@Override
protected void configure(GraphQL.Builder builder) {}
}
41 changes: 41 additions & 0 deletions instrumentation/graphql-java-12.0/library/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Manual Instrumentation for GraphQL Java

Provides OpenTelemetry instrumentation for [GraphQL Java](https://www.graphql-java.com/).

## Quickstart

### Add these dependencies to your project:

Replace `OPENTELEMETRY_VERSION` with the latest stable
[release](https://mvnrepository.com/artifact/io.opentelemetry). `Minimum version: 1.13.0`

For Maven, add to your `pom.xml` dependencies:

```xml

<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-graphql-java-12.0</artifactId>
<version>OPENTELEMETRY_VERSION</version>
</dependency>
</dependencies>
```

For Gradle, add to your dependencies:

```groovy
implementation("io.opentelemetry.instrumentation:opentelemetry-graphql-java-12.0:OPENTELEMETRY_VERSION")
```

### Usage

The instrumentation library provides a GraphQL Java `Instrumentation` implementation that can be
added to an instance of the `GraphQL` to provide OpenTelemetry-based spans.

```java
void configure(OpenTelemetry openTelemetry, GraphQL.Builder builder) {
GraphQLTracing tracing = GraphQLTracing.builder(openTelemetry).build();
builder.instrumentation(tracing.newInstrumentation());
}
```
9 changes: 9 additions & 0 deletions instrumentation/graphql-java-12.0/library/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
id("otel.library-instrumentation")
}

dependencies {
library("com.graphql-java:graphql-java:12.0")

testImplementation(project(":instrumentation:graphql-java-12.0:testing"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.graphql;

import graphql.ExecutionResult;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import javax.annotation.Nullable;

public class ExperimentalAttributesExtractor
laurit marked this conversation as resolved.
Show resolved Hide resolved
implements AttributesExtractor<InstrumentationExecutionParameters, ExecutionResult> {
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-graphql/src/enums/AttributeNames.ts
private static final AttributeKey<String> OPERATION_NAME =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these attributes are the same as in js instrumentation

AttributeKey.stringKey("graphql.operation.name");
private static final AttributeKey<String> OPERATION_TYPE =
AttributeKey.stringKey("graphql.operation.type");
private static final AttributeKey<String> GRAPHQL_SOURCE =
AttributeKey.stringKey("graphql.source");

@Override
public void onStart(
AttributesBuilder attributes,
Context parentContext,
InstrumentationExecutionParameters request) {}

@Override
public void onEnd(
AttributesBuilder attributes,
Context context,
InstrumentationExecutionParameters request,
@Nullable ExecutionResult response,
@Nullable Throwable error) {
OpenTelemetryInstrumentationState state = request.getInstrumentationState();
attributes.put(OPERATION_NAME, state.getOperationName());
if (state.getOperation() != null) {
attributes.put(OPERATION_TYPE, state.getOperation().name());
}
attributes.put(GRAPHQL_SOURCE, state.getQuery());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.graphql;

import graphql.ExecutionResult;
import graphql.execution.instrumentation.Instrumentation;
import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
import io.opentelemetry.instrumentation.api.instrumenter.SpanStatusExtractor;

@SuppressWarnings("AbbreviationAsWordInName")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed on slack, for library instrumentation I used GraphQL which isn't allowed by our checkstyle rules

public class GraphQLTracing {
laurit marked this conversation as resolved.
Show resolved Hide resolved
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.graphql-java-12.0";

/** Returns a new {@link GraphQLTracing} configured with the given {@link OpenTelemetry}. */
public static GraphQLTracing create(OpenTelemetry openTelemetry) {
return builder(openTelemetry).build();
}

/**
* Returns a new {@link GraphQLTracingBuilder} configured with the given {@link OpenTelemetry}.
*/
public static GraphQLTracingBuilder builder(OpenTelemetry openTelemetry) {
return new GraphQLTracingBuilder(openTelemetry);
}

private final Instrumenter<InstrumentationExecutionParameters, ExecutionResult> instrumenter;
private final boolean captureExperimentalSpanAttributes;
private final boolean sanitizeQuery;

GraphQLTracing(
OpenTelemetry openTelemetry,
boolean captureExperimentalSpanAttributes,
boolean sanitizeQuery) {
InstrumenterBuilder<InstrumentationExecutionParameters, ExecutionResult> builder =
Instrumenter.<InstrumentationExecutionParameters, ExecutionResult>builder(
openTelemetry, INSTRUMENTATION_NAME, ignored -> "GraphQL Query")
.setSpanStatusExtractor(
(instrumentationExecutionParameters, executionResult, error) -> {
if (!executionResult.getErrors().isEmpty()) {
return StatusCode.ERROR;
}
return SpanStatusExtractor.getDefault()
.extract(instrumentationExecutionParameters, executionResult, error);
});
if (captureExperimentalSpanAttributes) {
builder.addAttributesExtractor(new ExperimentalAttributesExtractor());
}

this.instrumenter = builder.newInstrumenter();
this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes;
this.sanitizeQuery = sanitizeQuery;
}

/**
* Returns a new {@link Instrumentation} that generates telemetry for received GraphQL requests.
*/
public Instrumentation newInstrumentation() {
return new OpenTelemetryInstrumentation(
instrumenter, captureExperimentalSpanAttributes, sanitizeQuery);
}
}
Loading