diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 562e2c8841..1ba7206f88 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/java-agent/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ByteBuddyElementMatchers.java b/java-agent/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ByteBuddyElementMatchers.java index 2d32c39fae..d75dec1750 100644 --- a/java-agent/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ByteBuddyElementMatchers.java +++ b/java-agent/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/ByteBuddyElementMatchers.java @@ -328,9 +328,8 @@ public class ByteBuddyElementMatchers { final Set checkedInterfaces = new HashSet<>(); while (declaringType != null) { - for (final MethodDescription methodDescription : - declaringType.getDeclaredMethods().filter(signatureMatcher)) { - if (matcher.matches(methodDescription)) { + for (final MethodDescription methodDescription : declaringType.getDeclaredMethods()) { + if (signatureMatcher.matches(methodDescription) && matcher.matches(methodDescription)) { return true; } } @@ -349,9 +348,8 @@ public class ByteBuddyElementMatchers { for (final TypeDefinition type : interfaces) { if (!checkedInterfaces.contains(type)) { checkedInterfaces.add(type); - for (final MethodDescription methodDescription : - type.getDeclaredMethods().filter(signatureMatcher)) { - if (matcher.matches(methodDescription)) { + for (final MethodDescription methodDescription : type.getDeclaredMethods()) { + if (signatureMatcher.matches(methodDescription) && matcher.matches(methodDescription)) { return true; } } diff --git a/java-agent/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/muzzle/MuzzleVersionScanPlugin.java b/java-agent/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/muzzle/MuzzleVersionScanPlugin.java index 384c533e5e..67179c3fb8 100644 --- a/java-agent/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/muzzle/MuzzleVersionScanPlugin.java +++ b/java-agent/agent-tooling/src/main/java/io/opentelemetry/auto/tooling/muzzle/MuzzleVersionScanPlugin.java @@ -53,8 +53,11 @@ public class MuzzleVersionScanPlugin { final ReferenceMatcher muzzle = (ReferenceMatcher) m.invoke(instrumenter); final List mismatches = muzzle.getMismatchedReferenceSources(userClassLoader); - final boolean passed = mismatches.size() == 0; - if (mismatches.size() > 0) {} + + final boolean classLoaderMatch = + ((Instrumenter.Default) instrumenter).classLoaderMatcher().matches(userClassLoader); + final boolean passed = mismatches.isEmpty() && classLoaderMatch; + if (passed && !assertPass) { System.err.println( "MUZZLE PASSED " @@ -64,6 +67,11 @@ public class MuzzleVersionScanPlugin { } else if (!passed && assertPass) { System.err.println( "FAILED MUZZLE VALIDATION: " + instrumenter.getClass().getName() + " mismatches:"); + + if (!classLoaderMatch) { + System.err.println("-- classloader mismatch"); + } + for (final Reference.Mismatch mismatch : mismatches) { System.err.println("-- " + mismatch); } diff --git a/java-agent/instrumentation/aws-java-sdk-1.11.0/aws-java-sdk-1.11.0.gradle b/java-agent/instrumentation/aws-java-sdk-1.11.0/aws-java-sdk-1.11.0.gradle index 42b81911f8..9adfa99ece 100644 --- a/java-agent/instrumentation/aws-java-sdk-1.11.0/aws-java-sdk-1.11.0.gradle +++ b/java-agent/instrumentation/aws-java-sdk-1.11.0/aws-java-sdk-1.11.0.gradle @@ -42,6 +42,12 @@ dependencies { testCompile group: 'com.amazonaws', name: 'aws-java-sdk-s3', version: '1.11.106' testCompile group: 'com.amazonaws', name: 'aws-java-sdk-rds', version: '1.11.106' testCompile group: 'com.amazonaws', name: 'aws-java-sdk-ec2', version: '1.11.106' + testCompile group: 'com.amazonaws', name: 'aws-java-sdk-kinesis', version: '1.11.106' + testCompile group: 'com.amazonaws', name: 'aws-java-sdk-sqs', version: '1.11.106' + testCompile group: 'com.amazonaws', name: 'aws-java-sdk-dynamodb', version: '1.11.106' + + // needed for kinesis: + testCompile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-cbor', version: versions.jackson test_before_1_11_106Compile(group: 'com.amazonaws', name: 'aws-java-sdk-s3', version: '1.11.0') { force = true @@ -52,10 +58,22 @@ dependencies { test_before_1_11_106Compile(group: 'com.amazonaws', name: 'aws-java-sdk-ec2', version: '1.11.0') { force = true } + test_before_1_11_106Compile(group: 'com.amazonaws', name: 'aws-java-sdk-kinesis', version: '1.11.0') { + force = true + } + test_before_1_11_106Compile(group: 'com.amazonaws', name: 'aws-java-sdk-sqs', version: '1.11.0') { + force = true + } + test_before_1_11_106Compile(group: 'com.amazonaws', name: 'aws-java-sdk-dynamodb', version: '1.11.0') { + force = true + } latestDepTestCompile group: 'com.amazonaws', name: 'aws-java-sdk-s3', version: '+' latestDepTestCompile group: 'com.amazonaws', name: 'aws-java-sdk-rds', version: '+' latestDepTestCompile group: 'com.amazonaws', name: 'aws-java-sdk-ec2', version: '+' + latestDepTestCompile group: 'com.amazonaws', name: 'aws-java-sdk-kinesis', version: '+' + latestDepTestCompile group: 'com.amazonaws', name: 'aws-java-sdk-sqs', version: '+' + latestDepTestCompile group: 'com.amazonaws', name: 'aws-java-sdk-dynamodb', version: '+' } test.dependsOn test_before_1_11_106 diff --git a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AWSClientInstrumentation.java b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AWSClientInstrumentation.java index 2bf5f04a04..b96ca1987b 100644 --- a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AWSClientInstrumentation.java +++ b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AWSClientInstrumentation.java @@ -5,8 +5,10 @@ import static net.bytebuddy.matcher.ElementMatchers.declaresField; import static net.bytebuddy.matcher.ElementMatchers.isConstructor; import static net.bytebuddy.matcher.ElementMatchers.named; +import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.handlers.RequestHandler2; import com.google.auto.service.AutoService; +import io.opentelemetry.auto.bootstrap.InstrumentationContext; import io.opentelemetry.auto.tooling.Instrumenter; import java.util.List; import java.util.Map; @@ -39,6 +41,7 @@ public final class AWSClientInstrumentation extends Instrumenter.Default { "io.opentelemetry.auto.decorator.ClientDecorator", "io.opentelemetry.auto.decorator.HttpClientDecorator", packageName + ".AwsSdkClientDecorator", + packageName + ".RequestMeta", packageName + ".TracingRequestHandler", }; } @@ -49,6 +52,11 @@ public final class AWSClientInstrumentation extends Instrumenter.Default { isConstructor(), AWSClientInstrumentation.class.getName() + "$AWSClientAdvice"); } + @Override + public Map contextStore() { + return singletonMap("com.amazonaws.AmazonWebServiceRequest", packageName + ".RequestMeta"); + } + public static class AWSClientAdvice { // Since we're instrumenting the constructor, we can't add onThrowable. @Advice.OnMethodExit(suppress = Throwable.class) @@ -62,7 +70,9 @@ public final class AWSClientInstrumentation extends Instrumenter.Default { } } if (!hasAgentHandler) { - handlers.add(TracingRequestHandler.INSTANCE); + handlers.add( + new TracingRequestHandler( + InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class))); } } } diff --git a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AWSHttpClientInstrumentation.java b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AWSHttpClientInstrumentation.java index e2b133aee4..4bc8426b97 100644 --- a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AWSHttpClientInstrumentation.java +++ b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AWSHttpClientInstrumentation.java @@ -1,6 +1,7 @@ package io.opentelemetry.auto.instrumentation.aws.v0; -import static io.opentelemetry.auto.instrumentation.aws.v0.AwsSdkClientDecorator.DECORATE; +import static io.opentelemetry.auto.instrumentation.aws.v0.OnErrorDecorator.DECORATE; +import static io.opentelemetry.auto.instrumentation.aws.v0.RequestMeta.SPAN_SCOPE_PAIR_CONTEXT_KEY; import static java.util.Collections.singletonMap; import static net.bytebuddy.matcher.ElementMatchers.isAbstract; import static net.bytebuddy.matcher.ElementMatchers.isMethod; @@ -41,10 +42,8 @@ public class AWSHttpClientInstrumentation extends Instrumenter.Default { public String[] helperClassNames() { return new String[] { "io.opentelemetry.auto.decorator.BaseDecorator", - "io.opentelemetry.auto.decorator.ClientDecorator", - "io.opentelemetry.auto.decorator.HttpClientDecorator", - packageName + ".AwsSdkClientDecorator", - packageName + ".TracingRequestHandler", + packageName + ".OnErrorDecorator", + packageName + ".RequestMeta", }; } @@ -61,10 +60,9 @@ public class AWSHttpClientInstrumentation extends Instrumenter.Default { @Advice.Argument(value = 0, optional = true) final Request request, @Advice.Thrown final Throwable throwable) { if (throwable != null) { - final SpanScopePair spanScopePair = - request.getHandlerContext(TracingRequestHandler.SPAN_SCOPE_PAIR_CONTEXT_KEY); + final SpanScopePair spanScopePair = request.getHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY); if (spanScopePair != null) { - request.addHandlerContext(TracingRequestHandler.SPAN_SCOPE_PAIR_CONTEXT_KEY, null); + request.addHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY, null); final Span span = spanScopePair.getSpan(); DECORATE.onError(span, throwable); DECORATE.beforeFinish(span); @@ -101,9 +99,9 @@ public class AWSHttpClientInstrumentation extends Instrumenter.Default { @Advice.Thrown final Throwable throwable) { if (throwable != null) { final SpanScopePair spanScopePair = - request.getHandlerContext(TracingRequestHandler.SPAN_SCOPE_PAIR_CONTEXT_KEY); + request.getHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY); if (spanScopePair != null) { - request.addHandlerContext(TracingRequestHandler.SPAN_SCOPE_PAIR_CONTEXT_KEY, null); + request.addHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY, null); final Span span = spanScopePair.getSpan(); DECORATE.onError(span, throwable); DECORATE.beforeFinish(span); diff --git a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AwsSdkClientDecorator.java b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AwsSdkClientDecorator.java index 4f56b1f805..c1a132e549 100644 --- a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AwsSdkClientDecorator.java +++ b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/AwsSdkClientDecorator.java @@ -1,9 +1,11 @@ package io.opentelemetry.auto.instrumentation.aws.v0; +import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.AmazonWebServiceResponse; import com.amazonaws.Request; import com.amazonaws.Response; import io.opentelemetry.auto.api.MoreTags; +import io.opentelemetry.auto.bootstrap.ContextStore; import io.opentelemetry.auto.decorator.HttpClientDecorator; import io.opentelemetry.trace.Span; import java.net.URI; @@ -12,12 +14,17 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class AwsSdkClientDecorator extends HttpClientDecorator { - public static final AwsSdkClientDecorator DECORATE = new AwsSdkClientDecorator(); static final String COMPONENT_NAME = "java-aws-sdk"; private final Map serviceNames = new ConcurrentHashMap<>(); private final Map operationNames = new ConcurrentHashMap<>(); + private final ContextStore contextStore; + + public AwsSdkClientDecorator( + final ContextStore contextStore) { + this.contextStore = contextStore; + } @Override public Span onRequest(final Span span, final Request request) { @@ -25,7 +32,8 @@ public class AwsSdkClientDecorator extends HttpClientDecorator awsOperation = request.getOriginalRequest().getClass(); + final AmazonWebServiceRequest originalRequest = request.getOriginalRequest(); + final Class awsOperation = originalRequest.getClass(); span.setAttribute("aws.agent", COMPONENT_NAME); span.setAttribute("aws.service", awsServiceName); @@ -36,6 +44,32 @@ public class AwsSdkClientDecorator extends HttpClientDecorator typeMatcher() { + return safeHasSuperType(named("com.amazonaws.AmazonWebServiceRequest")); + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".RequestMeta", + }; + } + + @Override + public Map, String> transformers() { + final Map, String> transformers = new HashMap<>(); + transformers.put( + named("setBucketName").and(takesArgument(0, String.class)), + RequestInstrumentation.class.getName() + "$BucketNameAdvice"); + transformers.put( + named("setQueueUrl").and(takesArgument(0, String.class)), + RequestInstrumentation.class.getName() + "$QueueUrlAdvice"); + transformers.put( + named("setQueueName").and(takesArgument(0, String.class)), + RequestInstrumentation.class.getName() + "$QueueNameAdvice"); + transformers.put( + named("setStreamName").and(takesArgument(0, String.class)), + RequestInstrumentation.class.getName() + "$StreamNameAdvice"); + transformers.put( + named("setTableName").and(takesArgument(0, String.class)), + RequestInstrumentation.class.getName() + "$TableNameAdvice"); + return transformers; + } + + @Override + public Map contextStore() { + return singletonMap("com.amazonaws.AmazonWebServiceRequest", packageName + ".RequestMeta"); + } + + public static class BucketNameAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) final String value, + @Advice.This final AmazonWebServiceRequest request) { + final ContextStore contextStore = + InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class); + RequestMeta requestMeta = contextStore.get(request); + if (requestMeta == null) { + requestMeta = new RequestMeta(); + contextStore.put(request, requestMeta); + } + requestMeta.setBucketName(value); + } + } + + public static class QueueUrlAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) final String value, + @Advice.This final AmazonWebServiceRequest request) { + final ContextStore contextStore = + InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class); + RequestMeta requestMeta = contextStore.get(request); + if (requestMeta == null) { + requestMeta = new RequestMeta(); + contextStore.put(request, requestMeta); + } + requestMeta.setQueueUrl(value); + } + } + + public static class QueueNameAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) final String value, + @Advice.This final AmazonWebServiceRequest request) { + final ContextStore contextStore = + InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class); + RequestMeta requestMeta = contextStore.get(request); + if (requestMeta == null) { + requestMeta = new RequestMeta(); + contextStore.put(request, requestMeta); + } + requestMeta.setQueueName(value); + } + } + + public static class StreamNameAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) final String value, + @Advice.This final AmazonWebServiceRequest request) { + final ContextStore contextStore = + InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class); + RequestMeta requestMeta = contextStore.get(request); + if (requestMeta == null) { + requestMeta = new RequestMeta(); + contextStore.put(request, requestMeta); + } + requestMeta.setStreamName(value); + } + } + + public static class TableNameAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void methodEnter( + @Advice.Argument(0) final String value, + @Advice.This final AmazonWebServiceRequest request) { + final ContextStore contextStore = + InstrumentationContext.get(AmazonWebServiceRequest.class, RequestMeta.class); + RequestMeta requestMeta = contextStore.get(request); + if (requestMeta == null) { + requestMeta = new RequestMeta(); + contextStore.put(request, requestMeta); + } + requestMeta.setTableName(value); + } + } +} diff --git a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/RequestMeta.java b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/RequestMeta.java new file mode 100644 index 0000000000..daa34ac552 --- /dev/null +++ b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/RequestMeta.java @@ -0,0 +1,19 @@ +package io.opentelemetry.auto.instrumentation.aws.v0; + +import com.amazonaws.handlers.HandlerContextKey; +import io.opentelemetry.auto.instrumentation.api.SpanScopePair; +import lombok.Data; + +@Data +public class RequestMeta { + // Note: aws1.x sdk doesn't have any truly async clients so we can store scope in request context + // safely. + public static final HandlerContextKey SPAN_SCOPE_PAIR_CONTEXT_KEY = + new HandlerContextKey<>("io.opentelemetry.auto.SpanScopePair"); + + private String bucketName; + private String queueUrl; + private String queueName; + private String streamName; + private String tableName; +} diff --git a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/TracingRequestHandler.java b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/TracingRequestHandler.java index e2b2590740..be0339ecb0 100644 --- a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/TracingRequestHandler.java +++ b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/main/java/io/opentelemetry/auto/instrumentation/aws/v0/TracingRequestHandler.java @@ -1,28 +1,27 @@ package io.opentelemetry.auto.instrumentation.aws.v0; -import static io.opentelemetry.auto.instrumentation.aws.v0.AwsSdkClientDecorator.DECORATE; +import static io.opentelemetry.auto.instrumentation.aws.v0.RequestMeta.SPAN_SCOPE_PAIR_CONTEXT_KEY; import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.Request; import com.amazonaws.Response; -import com.amazonaws.handlers.HandlerContextKey; import com.amazonaws.handlers.RequestHandler2; import io.opentelemetry.OpenTelemetry; +import io.opentelemetry.auto.bootstrap.ContextStore; import io.opentelemetry.auto.instrumentation.api.SpanScopePair; import io.opentelemetry.trace.Span; import io.opentelemetry.trace.Tracer; /** Tracing Request Handler */ public class TracingRequestHandler extends RequestHandler2 { - public static TracingRequestHandler INSTANCE = new TracingRequestHandler(); + public static final Tracer TRACER = OpenTelemetry.getTracerFactory().get("io.opentelemetry.auto"); - private static final Tracer TRACER = - OpenTelemetry.getTracerFactory().get("io.opentelemetry.auto"); + private final AwsSdkClientDecorator decorate; - // Note: aws1.x sdk doesn't have any truly async clients so we can store scope in request context - // safely. - public static final HandlerContextKey SPAN_SCOPE_PAIR_CONTEXT_KEY = - new HandlerContextKey<>("io.opentelemetry.auto.SpanScopePair"); + public TracingRequestHandler( + final ContextStore contextStore) { + decorate = new AwsSdkClientDecorator(contextStore); + } @Override public AmazonWebServiceRequest beforeMarshalling(final AmazonWebServiceRequest request) { @@ -32,8 +31,8 @@ public class TracingRequestHandler extends RequestHandler2 { @Override public void beforeRequest(final Request request) { final Span span = TRACER.spanBuilder("aws.http").startSpan(); - DECORATE.afterStart(span); - DECORATE.onRequest(span, request); + decorate.afterStart(span); + decorate.onRequest(span, request); request.addHandlerContext( SPAN_SCOPE_PAIR_CONTEXT_KEY, new SpanScopePair(span, TRACER.withSpan(span))); } @@ -45,8 +44,8 @@ public class TracingRequestHandler extends RequestHandler2 { request.addHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY, null); spanScopePair.getScope().close(); final Span span = spanScopePair.getSpan(); - DECORATE.onResponse(span, response); - DECORATE.beforeFinish(span); + decorate.onResponse(span, response); + decorate.beforeFinish(span); span.end(); } } @@ -58,8 +57,8 @@ public class TracingRequestHandler extends RequestHandler2 { request.addHandlerContext(SPAN_SCOPE_PAIR_CONTEXT_KEY, null); spanScopePair.getScope().close(); final Span span = spanScopePair.getSpan(); - DECORATE.onError(span, e); - DECORATE.beforeFinish(span); + decorate.onError(span, e); + decorate.beforeFinish(span); span.end(); } } diff --git a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/test/groovy/AWSClientTest.groovy b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/test/groovy/AWSClientTest.groovy index e7289dec56..fd51523d89 100644 --- a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/test/groovy/AWSClientTest.groovy +++ b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/test/groovy/AWSClientTest.groovy @@ -16,11 +16,18 @@ import com.amazonaws.client.builder.AwsClientBuilder import com.amazonaws.handlers.RequestHandler2 import com.amazonaws.regions.Regions import com.amazonaws.retry.PredefinedRetryPolicies +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder +import com.amazonaws.services.dynamodbv2.model.CreateTableRequest import com.amazonaws.services.ec2.AmazonEC2ClientBuilder +import com.amazonaws.services.kinesis.AmazonKinesisClientBuilder +import com.amazonaws.services.kinesis.model.DeleteStreamRequest import com.amazonaws.services.rds.AmazonRDSClientBuilder import com.amazonaws.services.rds.model.DeleteOptionGroupRequest import com.amazonaws.services.s3.AmazonS3Client import com.amazonaws.services.s3.AmazonS3ClientBuilder +import com.amazonaws.services.sqs.AmazonSQSClientBuilder +import com.amazonaws.services.sqs.model.CreateQueueRequest +import com.amazonaws.services.sqs.model.SendMessageRequest import io.opentelemetry.auto.api.MoreTags import io.opentelemetry.auto.api.SpanTypes import io.opentelemetry.auto.instrumentation.api.Tags @@ -144,6 +151,9 @@ class AWSClientTest extends AgentTestRunner { "aws.endpoint" "$server.address" "aws.operation" "${operation}Request" "aws.agent" "java-aws-sdk" + for (def addedTag : additionalTags) { + "$addedTag.key" "$addedTag.value" + } } } span(1) { @@ -156,7 +166,7 @@ class AWSClientTest extends AgentTestRunner { "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT "$Tags.PEER_HOSTNAME" "localhost" "$Tags.PEER_PORT" server.address.port - "$Tags.HTTP_URL" "$server.address/$url" + "$Tags.HTTP_URL" "${server.address}${path}" "$Tags.HTTP_METHOD" "$method" "$Tags.HTTP_STATUS" 200 } @@ -166,23 +176,41 @@ class AWSClientTest extends AgentTestRunner { server.lastRequest.headers.get("traceparent") == null where: - service | operation | method | url | handlerCount | call | body | client - "S3" | "CreateBucket" | "PUT" | "testbucket/" | 1 | { client -> client.createBucket("testbucket") } | "" | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true).withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() - "S3" | "GetObject" | "GET" | "someBucket/someKey" | 1 | { client -> client.getObject("someBucket", "someKey") } | "" | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true).withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() - "EC2" | "AllocateAddress" | "POST" | "" | 4 | { client -> client.allocateAddress() } | """ - - 59dbff89-35bd-4eac-99ed-be587EXAMPLE - 192.0.2.1 - standard - - """ | AmazonEC2ClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() - "RDS" | "DeleteOptionGroup" | "POST" | "" | 5 | { client -> client.deleteOptionGroup(new DeleteOptionGroupRequest()) } | """ + service | operation | method | path | handlerCount | client | call | additionalTags | body + "S3" | "CreateBucket" | "PUT" | "/testbucket/" | 1 | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true).withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { client -> client.createBucket("testbucket") } | ["aws.bucket.name": "testbucket"] | "" + "S3" | "GetObject" | "GET" | "/someBucket/someKey" | 1 | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true).withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { client -> client.getObject("someBucket", "someKey") } | ["aws.bucket.name": "someBucket"] | "" + "DynamoDBv2" | "CreateTable" | "POST" | "/" | 1 | AmazonDynamoDBClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createTable(new CreateTableRequest("sometable", null)) } | ["aws.table.name": "sometable"] | "" + "Kinesis" | "DeleteStream" | "POST" | "/" | 1 | AmazonKinesisClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.deleteStream(new DeleteStreamRequest().withStreamName("somestream")) } | ["aws.stream.name": "somestream"] | "" + "SQS" | "CreateQueue" | "POST" | "/" | 4 | AmazonSQSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createQueue(new CreateQueueRequest("somequeue")) } | ["aws.queue.name": "somequeue"] | """ + + https://queue.amazonaws.com/123456789012/MyQueue + 7a62c49f-347e-4fc4-9331-6e8e7a96aa73 + + """ + "SQS" | "SendMessage" | "POST" | "/someurl" | 4 | AmazonSQSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.sendMessage(new SendMessageRequest("someurl", "")) } | ["aws.queue.url": "someurl"] | """ + + + d41d8cd98f00b204e9800998ecf8427e + 3ae8f24a165a8cedc005670c81a27295 + 5fea7756-0ea4-451a-a703-a558b933e274 + + 27daac76-34dd-47df-bd01-1f6e873584a0 + + """ + "EC2" | "AllocateAddress" | "POST" | "/" | 4 | AmazonEC2ClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { client -> client.allocateAddress() } | [:] | """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + 192.0.2.1 + standard + + """ + "RDS" | "DeleteOptionGroup" | "POST" | "/" | 5 | AmazonRDSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { client -> client.deleteOptionGroup(new DeleteOptionGroupRequest()) } | [:] | """ 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 - """ | AmazonRDSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() + """ } def "send #operation request to closed port"() { @@ -213,6 +241,9 @@ class AWSClientTest extends AgentTestRunner { "aws.endpoint" "http://localhost:${UNUSABLE_PORT}" "aws.operation" "${operation}Request" "aws.agent" "java-aws-sdk" + for (def addedTag : additionalTags) { + "$addedTag.key" "$addedTag.value" + } errorTags SdkClientException, ~/Unable to execute HTTP request/ } } @@ -235,8 +266,8 @@ class AWSClientTest extends AgentTestRunner { } where: - service | operation | method | url | call | body | client - "S3" | "GetObject" | "GET" | "someBucket/someKey" | { client -> client.getObject("someBucket", "someKey") } | "" | new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN, new ClientConfiguration().withRetryPolicy(PredefinedRetryPolicies.getDefaultRetryPolicyWithCustomMaxRetries(0))).withEndpoint("http://localhost:${UNUSABLE_PORT}") + service | operation | method | url | call | additionalTags | body | client + "S3" | "GetObject" | "GET" | "someBucket/someKey" | { client -> client.getObject("someBucket", "someKey") } | ["aws.bucket.name": "someBucket"] | "" | new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN, new ClientConfiguration().withRetryPolicy(PredefinedRetryPolicies.getDefaultRetryPolicyWithCustomMaxRetries(0))).withEndpoint("http://localhost:${UNUSABLE_PORT}") } def "naughty request handler doesn't break the trace"() { @@ -318,6 +349,7 @@ class AWSClientTest extends AgentTestRunner { "aws.endpoint" "$server.address" "aws.operation" "GetObjectRequest" "aws.agent" "java-aws-sdk" + "aws.bucket.name" "someBucket" try { errorTags AmazonClientException, ~/Unable to execute HTTP request/ } catch (AssertionError e) { diff --git a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/test_before_1_11_106/groovy/AWSClientTest.groovy b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/test_before_1_11_106/groovy/AWSClientTest.groovy index 69227b2e8c..a6b74768f1 100644 --- a/java-agent/instrumentation/aws-java-sdk-1.11.0/src/test_before_1_11_106/groovy/AWSClientTest.groovy +++ b/java-agent/instrumentation/aws-java-sdk-1.11.0/src/test_before_1_11_106/groovy/AWSClientTest.groovy @@ -114,6 +114,9 @@ class AWSClientTest extends AgentTestRunner { "aws.endpoint" "$server.address" "aws.operation" "${operation}Request" "aws.agent" "java-aws-sdk" + for (def addedTag : additionalTags) { + "$addedTag.key" "$addedTag.value" + } } } span(1) { @@ -126,7 +129,7 @@ class AWSClientTest extends AgentTestRunner { "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT "$Tags.PEER_HOSTNAME" "localhost" "$Tags.PEER_PORT" server.address.port - "$Tags.HTTP_URL" "$server.address/$url" + "$Tags.HTTP_URL" "${server.address}${path}" "$Tags.HTTP_METHOD" "$method" "$Tags.HTTP_STATUS" 200 } @@ -136,23 +139,23 @@ class AWSClientTest extends AgentTestRunner { server.lastRequest.headers.get("traceparent") == null where: - service | operation | method | url | handlerCount | call | body | client - "S3" | "CreateBucket" | "PUT" | "testbucket/" | 1 | { client -> client.setS3ClientOptions(S3ClientOptions.builder().setPathStyleAccess(true).build()); client.createBucket("testbucket") } | "" | new AmazonS3Client().withEndpoint("http://localhost:$server.address.port") - "S3" | "GetObject" | "GET" | "someBucket/someKey" | 1 | { client -> client.getObject("someBucket", "someKey") } | "" | new AmazonS3Client().withEndpoint("http://localhost:$server.address.port") - "EC2" | "AllocateAddress" | "POST" | "" | 4 | { client -> client.allocateAddress() } | """ + service | operation | method | path | handlerCount | client | additionalTags | call | body + "S3" | "CreateBucket" | "PUT" | "/testbucket/" | 1 | new AmazonS3Client().withEndpoint("http://localhost:$server.address.port") | ["aws.bucket.name": "testbucket"] | { client -> client.setS3ClientOptions(S3ClientOptions.builder().setPathStyleAccess(true).build()); client.createBucket("testbucket") } | "" + "S3" | "GetObject" | "GET" | "/someBucket/someKey" | 1 | new AmazonS3Client().withEndpoint("http://localhost:$server.address.port") | ["aws.bucket.name": "someBucket"] | { client -> client.getObject("someBucket", "someKey") } | "" + "EC2" | "AllocateAddress" | "POST" | "/" | 4 | new AmazonEC2Client().withEndpoint("http://localhost:$server.address.port") | [:] | { client -> client.allocateAddress() } | """ 59dbff89-35bd-4eac-99ed-be587EXAMPLE 192.0.2.1 standard - """ | new AmazonEC2Client().withEndpoint("http://localhost:$server.address.port") - "RDS" | "DeleteOptionGroup" | "POST" | "" | 1 | { client -> client.deleteOptionGroup(new DeleteOptionGroupRequest()) } | """ + """ + "RDS" | "DeleteOptionGroup" | "POST" | "/" | 1 | new AmazonRDSClient().withEndpoint("http://localhost:$server.address.port") | [:] | { client -> client.deleteOptionGroup(new DeleteOptionGroupRequest()) } | """ 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 - """ | new AmazonRDSClient().withEndpoint("http://localhost:$server.address.port") + """ } def "send #operation request to closed port"() { @@ -183,6 +186,9 @@ class AWSClientTest extends AgentTestRunner { "aws.endpoint" "http://localhost:${UNUSABLE_PORT}" "aws.operation" "${operation}Request" "aws.agent" "java-aws-sdk" + for (def addedTag : additionalTags) { + "$addedTag.key" "$addedTag.value" + } errorTags AmazonClientException, ~/Unable to execute HTTP request/ } } @@ -205,8 +211,8 @@ class AWSClientTest extends AgentTestRunner { } where: - service | operation | method | url | call | body | client - "S3" | "GetObject" | "GET" | "someBucket/someKey" | { client -> client.getObject("someBucket", "someKey") } | "" | new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN, new ClientConfiguration().withRetryPolicy(PredefinedRetryPolicies.getDefaultRetryPolicyWithCustomMaxRetries(0))).withEndpoint("http://localhost:${UNUSABLE_PORT}") + service | operation | method | url | call | additionalTags | body | client + "S3" | "GetObject" | "GET" | "someBucket/someKey" | { client -> client.getObject("someBucket", "someKey") } | ["aws.bucket.name": "someBucket"] | "" | new AmazonS3Client(CREDENTIALS_PROVIDER_CHAIN, new ClientConfiguration().withRetryPolicy(PredefinedRetryPolicies.getDefaultRetryPolicyWithCustomMaxRetries(0))).withEndpoint("http://localhost:${UNUSABLE_PORT}") } def "naughty request handler doesn't break the trace"() { @@ -243,6 +249,7 @@ class AWSClientTest extends AgentTestRunner { "aws.endpoint" "https://s3.amazonaws.com" "aws.operation" "GetObjectRequest" "aws.agent" "java-aws-sdk" + "aws.bucket.name" "someBucket" errorTags RuntimeException, "bad handler" } } @@ -288,6 +295,7 @@ class AWSClientTest extends AgentTestRunner { "aws.endpoint" "http://localhost:$server.address.port" "aws.operation" "GetObjectRequest" "aws.agent" "java-aws-sdk" + "aws.bucket.name" "someBucket" errorTags AmazonClientException, ~/Unable to execute HTTP request/ } } diff --git a/java-agent/instrumentation/aws-java-sdk-2.2/src/main/java8/io/opentelemetry/auto/instrumentation/aws/v2/AwsSdkClientDecorator.java b/java-agent/instrumentation/aws-java-sdk-2.2/src/main/java8/io/opentelemetry/auto/instrumentation/aws/v2/AwsSdkClientDecorator.java index f51df4447c..9f33559fe9 100644 --- a/java-agent/instrumentation/aws-java-sdk-2.2/src/main/java8/io/opentelemetry/auto/instrumentation/aws/v2/AwsSdkClientDecorator.java +++ b/java-agent/instrumentation/aws-java-sdk-2.2/src/main/java8/io/opentelemetry/auto/instrumentation/aws/v2/AwsSdkClientDecorator.java @@ -22,21 +22,21 @@ public class AwsSdkClientDecorator extends HttpClientDecorator span.setAttribute("aws.bucket.name", name)); - // DynamoDB - request - .getValueForField("TableName", String.class) - .ifPresent(name -> span.setAttribute("aws.table.name", name)); // SQS - request - .getValueForField("QueueName", String.class) - .ifPresent(name -> span.setAttribute("aws.queue.name", name)); request .getValueForField("QueueUrl", String.class) .ifPresent(name -> span.setAttribute("aws.queue.url", name)); + request + .getValueForField("QueueName", String.class) + .ifPresent(name -> span.setAttribute("aws.queue.name", name)); // Kinesis request .getValueForField("StreamName", String.class) .ifPresent(name -> span.setAttribute("aws.stream.name", name)); + // DynamoDB + request + .getValueForField("TableName", String.class) + .ifPresent(name -> span.setAttribute("aws.table.name", name)); return span; } diff --git a/java-agent/instrumentation/grpc-1.5/src/main/java/io/opentelemetry/auto/instrumentation/grpc/server/GrpcExtractAdapter.java b/java-agent/instrumentation/grpc-1.5/src/main/java/io/opentelemetry/auto/instrumentation/grpc/server/GrpcExtractAdapter.java index da79cb5729..0d9991ad56 100644 --- a/java-agent/instrumentation/grpc-1.5/src/main/java/io/opentelemetry/auto/instrumentation/grpc/server/GrpcExtractAdapter.java +++ b/java-agent/instrumentation/grpc-1.5/src/main/java/io/opentelemetry/auto/instrumentation/grpc/server/GrpcExtractAdapter.java @@ -2,6 +2,8 @@ package io.opentelemetry.auto.instrumentation.grpc.server; import io.grpc.Metadata; import io.opentelemetry.auto.instrumentation.api.AgentPropagation; +import java.util.ArrayList; +import java.util.List; public final class GrpcExtractAdapter implements AgentPropagation.Getter { @@ -9,7 +11,15 @@ public final class GrpcExtractAdapter implements AgentPropagation.Getter keys(final Metadata carrier) { - return carrier.keys(); + final List keys = new ArrayList<>(); + + for (final String key : carrier.keys()) { + if (!key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { + keys.add(key); + } + } + + return keys; } @Override diff --git a/java-agent/instrumentation/grpc-1.5/src/test/groovy/GrpcTest.groovy b/java-agent/instrumentation/grpc-1.5/src/test/groovy/GrpcTest.groovy index ca0476fe31..1ec6452867 100644 --- a/java-agent/instrumentation/grpc-1.5/src/test/groovy/GrpcTest.groovy +++ b/java-agent/instrumentation/grpc-1.5/src/test/groovy/GrpcTest.groovy @@ -2,6 +2,7 @@ import example.GreeterGrpc import example.Helloworld import io.grpc.BindableService import io.grpc.ManagedChannel +import io.grpc.Metadata import io.grpc.Server import io.grpc.Status import io.grpc.StatusRuntimeException @@ -11,6 +12,7 @@ import io.grpc.stub.StreamObserver import io.opentelemetry.auto.api.MoreTags import io.opentelemetry.auto.api.SpanTypes import io.opentelemetry.auto.instrumentation.api.Tags +import io.opentelemetry.auto.instrumentation.grpc.server.GrpcExtractAdapter import io.opentelemetry.auto.test.AgentTestRunner import io.opentelemetry.sdk.trace.SpanData @@ -268,4 +270,17 @@ class GrpcTest extends AgentTestRunner { "Status - description" | Status.PERMISSION_DENIED.withDescription("some description") "StatusRuntime - description" | Status.UNIMPLEMENTED.withDescription("some description") } + + def "skip binary headers"() { + setup: + def meta = new Metadata() + meta.put(Metadata.Key. of("test", Metadata.ASCII_STRING_MARSHALLER), "val") + meta.put(Metadata.Key. of("test-bin", Metadata.BINARY_BYTE_MARSHALLER), "bin-val".bytes) + + when: + def keys = GrpcExtractAdapter.GETTER.keys(meta) + + then: + keys == ["test"] + } } diff --git a/java-agent/instrumentation/jax-rs-annotations-1/jax-rs-annotations-1.gradle b/java-agent/instrumentation/jax-rs-annotations-1/jax-rs-annotations-1.gradle index 320d759b31..da43f865e3 100644 --- a/java-agent/instrumentation/jax-rs-annotations-1/jax-rs-annotations-1.gradle +++ b/java-agent/instrumentation/jax-rs-annotations-1/jax-rs-annotations-1.gradle @@ -4,7 +4,11 @@ muzzle { module = "jsr311-api" versions = "[0.5,)" } - // Muzzle doesn't detect the classLoaderMatcher, so we can't assert fail for v2 api. + fail { + group = "javax.ws.rs" + module = "javax.ws.rs-api" + versions = "[,]" + } } apply from: "${rootDir}/gradle/java.gradle" diff --git a/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/DataSourceDecorator.java b/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/DataSourceDecorator.java new file mode 100644 index 0000000000..158f5f5aae --- /dev/null +++ b/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/DataSourceDecorator.java @@ -0,0 +1,22 @@ +package io.opentelemetry.auto.instrumentation.jdbc; + +import io.opentelemetry.auto.decorator.BaseDecorator; + +public class DataSourceDecorator extends BaseDecorator { + public static final DataSourceDecorator DECORATE = new DataSourceDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"jdbc-datasource"}; + } + + @Override + protected String component() { + return "java-jdbc-connection"; + } + + @Override + protected String spanType() { + return null; + } +} diff --git a/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/DataSourceInstrumentation.java b/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/DataSourceInstrumentation.java new file mode 100644 index 0000000000..c441e01a54 --- /dev/null +++ b/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/DataSourceInstrumentation.java @@ -0,0 +1,81 @@ +package io.opentelemetry.auto.instrumentation.jdbc; + +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.activateSpan; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.activeSpan; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.startSpan; +import static io.opentelemetry.auto.instrumentation.jdbc.DataSourceDecorator.DECORATE; +import static io.opentelemetry.auto.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.auto.api.MoreTags; +import io.opentelemetry.auto.instrumentation.api.AgentScope; +import io.opentelemetry.auto.instrumentation.api.AgentSpan; +import io.opentelemetry.auto.tooling.Instrumenter; +import java.util.Map; +import javax.sql.DataSource; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class DataSourceInstrumentation extends Instrumenter.Default { + public DataSourceInstrumentation() { + super("jdbc-datasource"); + } + + @Override + public boolean defaultEnabled() { + return false; + } + + @Override + public String[] helperClassNames() { + return new String[] { + "io.opentelemetry.auto.decorator.BaseDecorator", packageName + ".DataSourceDecorator", + }; + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()).and(safeHasSuperType(named("javax.sql.DataSource"))); + } + + @Override + public Map, String> transformers() { + return singletonMap(named("getConnection"), GetConnectionAdvice.class.getName()); + } + + public static class GetConnectionAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope start(@Advice.This final DataSource ds) { + if (activeSpan() == null) { + // Don't want to generate a new top-level span + return null; + } + + final AgentSpan span = startSpan("database.connection"); + DECORATE.afterStart(span); + + span.setAttribute(MoreTags.RESOURCE_NAME, ds.getClass().getSimpleName() + ".getConnection"); + + return activateSpan(span, true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + DECORATE.onError(scope, throwable); + DECORATE.beforeFinish(scope); + scope.close(); + } + } +} diff --git a/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/JDBCDecorator.java b/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/JDBCDecorator.java index bdbb1993df..a80520e4f1 100644 --- a/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/JDBCDecorator.java +++ b/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/JDBCDecorator.java @@ -69,7 +69,12 @@ public class JDBCDecorator extends DatabaseClientDecorator { final DatabaseMetaData metaData = connection.getMetaData(); final String url = metaData.getURL(); if (url != null) { - dbInfo = JDBCConnectionUrlParser.parse(url, connection.getClientInfo()); + try { + dbInfo = JDBCConnectionUrlParser.parse(url, connection.getClientInfo()); + } catch (final Exception ex) { + // getClientInfo is likely not allowed. + dbInfo = JDBCConnectionUrlParser.parse(url, null); + } } else { dbInfo = DBInfo.DEFAULT; } diff --git a/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/JDBCUtils.java b/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/JDBCUtils.java index 9fb3cd5707..3eaaddeb03 100644 --- a/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/JDBCUtils.java +++ b/java-agent/instrumentation/jdbc/src/main/java/io/opentelemetry/auto/instrumentation/jdbc/JDBCUtils.java @@ -30,13 +30,16 @@ public abstract class JDBCUtils { connection = connection.unwrap(Connection.class); } } catch (final Exception | AbstractMethodError e) { - // Attempt to work around c3po delegating to an connection that doesn't support unwrapping. - final Class connectionClass = connection.getClass(); - if (connectionClass.getName().equals("com.mchange.v2.c3p0.impl.NewProxyConnection")) { - final Field inner = connectionClass.getDeclaredField("inner"); - inner.setAccessible(true); - c3poField = inner; - return (Connection) c3poField.get(connection); + if (connection != null) { + // Attempt to work around c3po delegating to an connection that doesn't support + // unwrapping. + final Class connectionClass = connection.getClass(); + if (connectionClass.getName().equals("com.mchange.v2.c3p0.impl.NewProxyConnection")) { + final Field inner = connectionClass.getDeclaredField("inner"); + inner.setAccessible(true); + c3poField = inner; + return (Connection) c3poField.get(connection); + } } // perhaps wrapping isn't supported? diff --git a/java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy b/java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy index b937d2eff3..b9b0bed357 100644 --- a/java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy +++ b/java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy @@ -6,11 +6,15 @@ import io.opentelemetry.auto.api.MoreTags import io.opentelemetry.auto.api.SpanTypes import io.opentelemetry.auto.instrumentation.api.Tags import io.opentelemetry.auto.test.AgentTestRunner +import org.apache.derby.jdbc.EmbeddedDataSource import org.apache.derby.jdbc.EmbeddedDriver import org.h2.Driver +import org.h2.jdbcx.JdbcDataSource import org.hsqldb.jdbc.JDBCDriver import spock.lang.Shared import spock.lang.Unroll +import test.TestConnection +import test.TestStatement import javax.sql.DataSource import java.sql.CallableStatement @@ -24,6 +28,9 @@ import static io.opentelemetry.auto.test.utils.TraceUtils.basicSpan import static io.opentelemetry.auto.test.utils.TraceUtils.runUnderTrace class JDBCInstrumentationTest extends AgentTestRunner { + static { + System.setProperty("opentelemetry.auto.integration.jdbc-datasource.enabled", "true") + } @Shared def dbName = "jdbcUnitTest" @@ -476,7 +483,6 @@ class JDBCInstrumentationTest extends AgentTestRunner { "derby" | cpDatasources.get("hikari").get("derby").getConnection() | "APP" | "CREATE TABLE PS_DERBY_HIKARI (id INTEGER not NULL, PRIMARY KEY ( id ))" "h2" | cpDatasources.get("c3p0").get("h2").getConnection() | null | "CREATE TABLE PS_H2_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" "derby" | cpDatasources.get("c3p0").get("derby").getConnection() | "APP" | "CREATE TABLE PS_DERBY_C3P0 (id INTEGER not NULL, PRIMARY KEY ( id ))" - } @Unroll @@ -486,7 +492,7 @@ class JDBCInstrumentationTest extends AgentTestRunner { when: try { - connection = new DummyThrowingConnection() + connection = new TestConnection(true) } catch (Exception e) { connection = driverClass.connect(url, null) } @@ -550,6 +556,110 @@ class JDBCInstrumentationTest extends AgentTestRunner { false | "derby" | new EmbeddedDriver() | "jdbc:derby:memory:" + dbName + ";create=true" | "APP" | "SELECT 3 FROM SYSIBM.SYSDUMMY1" } + def "calling #datasource.class.simpleName getConnection generates a span when under existing trace"() { + setup: + assert datasource instanceof DataSource + init?.call(datasource) + + when: + datasource.getConnection().close() + + then: + !TEST_WRITER.traces.any { it.any { it.name == "database.connection" } } + TEST_WRITER.clear() + + when: + runUnderTrace("parent") { + datasource.getConnection().close() + } + + then: + assertTraces(1) { + trace(0, recursive ? 3 : 2) { + basicSpan(it, 0, "parent") + + span(1) { + operationName "database.connection" + childOf span(0) + tags { + "$MoreTags.RESOURCE_NAME" "${datasource.class.simpleName}.getConnection" + "$Tags.COMPONENT" "java-jdbc-connection" + } + } + if (recursive) { + span(2) { + operationName "database.connection" + childOf span(1) + tags { + "$MoreTags.RESOURCE_NAME" "${datasource.class.simpleName}.getConnection" + "$Tags.COMPONENT" "java-jdbc-connection" + } + } + } + } + } + + where: + datasource | init + new JdbcDataSource() | { ds -> ds.setURL(jdbcUrls.get("h2")) } + new EmbeddedDataSource() | { ds -> ds.jdbcurl = jdbcUrls.get("derby") } + cpDatasources.get("hikari").get("h2") | null + cpDatasources.get("hikari").get("derby") | null + cpDatasources.get("c3p0").get("h2") | null + cpDatasources.get("c3p0").get("derby") | null + + // Tomcat's pool doesn't work because the getConnection method is + // implemented in a parent class that doesn't implement DataSource + + recursive = datasource instanceof EmbeddedDataSource + } + + def "test getClientInfo exception"() { + setup: + Connection connection = new TestConnection(false) + + when: + Statement statement = null + runUnderTrace("parent") { + statement = connection.createStatement() + return statement.executeQuery(query) + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + operationName "database.query" + childOf span(0) + errored false + tags { + "$MoreTags.SERVICE_NAME" database + "$MoreTags.RESOURCE_NAME" query + "$MoreTags.SPAN_TYPE" SpanTypes.SQL + "$Tags.COMPONENT" "java-jdbc-statement" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" database + "$Tags.DB_STATEMENT" query + "span.origin.type" TestStatement.name + } + } + } + } + + cleanup: + if (statement != null) { + statement.close() + } + if (connection != null) { + connection.close() + } + + where: + database = "testdb" + query = "testing 123" + } + @Unroll def "#connectionPoolName connections should be cached in case of wrapped connections"() { setup: diff --git a/java-agent/instrumentation/jdbc/src/test/groovy/DummyThrowingConnection.groovy b/java-agent/instrumentation/jdbc/src/test/groovy/test/TestConnection.groovy similarity index 91% rename from java-agent/instrumentation/jdbc/src/test/groovy/DummyThrowingConnection.groovy rename to java-agent/instrumentation/jdbc/src/test/groovy/test/TestConnection.groovy index 018c27c172..a52de68ef3 100644 --- a/java-agent/instrumentation/jdbc/src/test/groovy/DummyThrowingConnection.groovy +++ b/java-agent/instrumentation/jdbc/src/test/groovy/test/TestConnection.groovy @@ -1,3 +1,5 @@ +package test + import java.sql.Array import java.sql.Blob import java.sql.CallableStatement @@ -17,16 +19,19 @@ import java.util.concurrent.Executor /** - * A JDBC connection class that throws an exception in the constructor, used to test + * A JDBC connection class that optionally throws an exception in the constructor, used to test */ -class DummyThrowingConnection implements Connection { - DummyThrowingConnection() { - throw new RuntimeException("Dummy exception") +class TestConnection implements Connection { + TestConnection(boolean throwException) { + if (throwException) { + throw new RuntimeException("connection exception") + } } + @Override Statement createStatement() throws SQLException { - return null + return new TestStatement(this) } @Override @@ -76,7 +81,7 @@ class DummyThrowingConnection implements Connection { @Override DatabaseMetaData getMetaData() throws SQLException { - return null + return new TestDatabaseMetaData() } @Override @@ -241,12 +246,12 @@ class DummyThrowingConnection implements Connection { @Override String getClientInfo(String name) throws SQLException { - return null + throw new UnsupportedOperationException("Test 123") } @Override Properties getClientInfo() throws SQLException { - return null + throw new UnsupportedOperationException("Test 123") } @Override diff --git a/java-agent/instrumentation/jdbc/src/test/groovy/test/TestDatabaseMetaData.groovy b/java-agent/instrumentation/jdbc/src/test/groovy/test/TestDatabaseMetaData.groovy new file mode 100644 index 0000000000..198f1832dd --- /dev/null +++ b/java-agent/instrumentation/jdbc/src/test/groovy/test/TestDatabaseMetaData.groovy @@ -0,0 +1,889 @@ +package test + +import java.sql.Connection +import java.sql.DatabaseMetaData +import java.sql.ResultSet +import java.sql.RowIdLifetime +import java.sql.SQLException + +class TestDatabaseMetaData implements DatabaseMetaData { + @Override + boolean allProceduresAreCallable() throws SQLException { + return false + } + + @Override + boolean allTablesAreSelectable() throws SQLException { + return false + } + + @Override + String getURL() throws SQLException { + return "jdbc:testdb://localhost" + } + + @Override + String getUserName() throws SQLException { + return null + } + + @Override + boolean isReadOnly() throws SQLException { + return false + } + + @Override + boolean nullsAreSortedHigh() throws SQLException { + return false + } + + @Override + boolean nullsAreSortedLow() throws SQLException { + return false + } + + @Override + boolean nullsAreSortedAtStart() throws SQLException { + return false + } + + @Override + boolean nullsAreSortedAtEnd() throws SQLException { + return false + } + + @Override + String getDatabaseProductName() throws SQLException { + return null + } + + @Override + String getDatabaseProductVersion() throws SQLException { + return null + } + + @Override + String getDriverName() throws SQLException { + return null + } + + @Override + String getDriverVersion() throws SQLException { + return null + } + + @Override + int getDriverMajorVersion() { + return 0 + } + + @Override + int getDriverMinorVersion() { + return 0 + } + + @Override + boolean usesLocalFiles() throws SQLException { + return false + } + + @Override + boolean usesLocalFilePerTable() throws SQLException { + return false + } + + @Override + boolean supportsMixedCaseIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesUpperCaseIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesLowerCaseIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesMixedCaseIdentifiers() throws SQLException { + return false + } + + @Override + boolean supportsMixedCaseQuotedIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesUpperCaseQuotedIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesLowerCaseQuotedIdentifiers() throws SQLException { + return false + } + + @Override + boolean storesMixedCaseQuotedIdentifiers() throws SQLException { + return false + } + + @Override + String getIdentifierQuoteString() throws SQLException { + return null + } + + @Override + String getSQLKeywords() throws SQLException { + return null + } + + @Override + String getNumericFunctions() throws SQLException { + return null + } + + @Override + String getStringFunctions() throws SQLException { + return null + } + + @Override + String getSystemFunctions() throws SQLException { + return null + } + + @Override + String getTimeDateFunctions() throws SQLException { + return null + } + + @Override + String getSearchStringEscape() throws SQLException { + return null + } + + @Override + String getExtraNameCharacters() throws SQLException { + return null + } + + @Override + boolean supportsAlterTableWithAddColumn() throws SQLException { + return false + } + + @Override + boolean supportsAlterTableWithDropColumn() throws SQLException { + return false + } + + @Override + boolean supportsColumnAliasing() throws SQLException { + return false + } + + @Override + boolean nullPlusNonNullIsNull() throws SQLException { + return false + } + + @Override + boolean supportsConvert() throws SQLException { + return false + } + + @Override + boolean supportsConvert(int fromType, int toType) throws SQLException { + return false + } + + @Override + boolean supportsTableCorrelationNames() throws SQLException { + return false + } + + @Override + boolean supportsDifferentTableCorrelationNames() throws SQLException { + return false + } + + @Override + boolean supportsExpressionsInOrderBy() throws SQLException { + return false + } + + @Override + boolean supportsOrderByUnrelated() throws SQLException { + return false + } + + @Override + boolean supportsGroupBy() throws SQLException { + return false + } + + @Override + boolean supportsGroupByUnrelated() throws SQLException { + return false + } + + @Override + boolean supportsGroupByBeyondSelect() throws SQLException { + return false + } + + @Override + boolean supportsLikeEscapeClause() throws SQLException { + return false + } + + @Override + boolean supportsMultipleResultSets() throws SQLException { + return false + } + + @Override + boolean supportsMultipleTransactions() throws SQLException { + return false + } + + @Override + boolean supportsNonNullableColumns() throws SQLException { + return false + } + + @Override + boolean supportsMinimumSQLGrammar() throws SQLException { + return false + } + + @Override + boolean supportsCoreSQLGrammar() throws SQLException { + return false + } + + @Override + boolean supportsExtendedSQLGrammar() throws SQLException { + return false + } + + @Override + boolean supportsANSI92EntryLevelSQL() throws SQLException { + return false + } + + @Override + boolean supportsANSI92IntermediateSQL() throws SQLException { + return false + } + + @Override + boolean supportsANSI92FullSQL() throws SQLException { + return false + } + + @Override + boolean supportsIntegrityEnhancementFacility() throws SQLException { + return false + } + + @Override + boolean supportsOuterJoins() throws SQLException { + return false + } + + @Override + boolean supportsFullOuterJoins() throws SQLException { + return false + } + + @Override + boolean supportsLimitedOuterJoins() throws SQLException { + return false + } + + @Override + String getSchemaTerm() throws SQLException { + return null + } + + @Override + String getProcedureTerm() throws SQLException { + return null + } + + @Override + String getCatalogTerm() throws SQLException { + return null + } + + @Override + boolean isCatalogAtStart() throws SQLException { + return false + } + + @Override + String getCatalogSeparator() throws SQLException { + return null + } + + @Override + boolean supportsSchemasInDataManipulation() throws SQLException { + return false + } + + @Override + boolean supportsSchemasInProcedureCalls() throws SQLException { + return false + } + + @Override + boolean supportsSchemasInTableDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsSchemasInIndexDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsCatalogsInDataManipulation() throws SQLException { + return false + } + + @Override + boolean supportsCatalogsInProcedureCalls() throws SQLException { + return false + } + + @Override + boolean supportsCatalogsInTableDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsCatalogsInIndexDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { + return false + } + + @Override + boolean supportsPositionedDelete() throws SQLException { + return false + } + + @Override + boolean supportsPositionedUpdate() throws SQLException { + return false + } + + @Override + boolean supportsSelectForUpdate() throws SQLException { + return false + } + + @Override + boolean supportsStoredProcedures() throws SQLException { + return false + } + + @Override + boolean supportsSubqueriesInComparisons() throws SQLException { + return false + } + + @Override + boolean supportsSubqueriesInExists() throws SQLException { + return false + } + + @Override + boolean supportsSubqueriesInIns() throws SQLException { + return false + } + + @Override + boolean supportsSubqueriesInQuantifieds() throws SQLException { + return false + } + + @Override + boolean supportsCorrelatedSubqueries() throws SQLException { + return false + } + + @Override + boolean supportsUnion() throws SQLException { + return false + } + + @Override + boolean supportsUnionAll() throws SQLException { + return false + } + + @Override + boolean supportsOpenCursorsAcrossCommit() throws SQLException { + return false + } + + @Override + boolean supportsOpenCursorsAcrossRollback() throws SQLException { + return false + } + + @Override + boolean supportsOpenStatementsAcrossCommit() throws SQLException { + return false + } + + @Override + boolean supportsOpenStatementsAcrossRollback() throws SQLException { + return false + } + + @Override + int getMaxBinaryLiteralLength() throws SQLException { + return 0 + } + + @Override + int getMaxCharLiteralLength() throws SQLException { + return 0 + } + + @Override + int getMaxColumnNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxColumnsInGroupBy() throws SQLException { + return 0 + } + + @Override + int getMaxColumnsInIndex() throws SQLException { + return 0 + } + + @Override + int getMaxColumnsInOrderBy() throws SQLException { + return 0 + } + + @Override + int getMaxColumnsInSelect() throws SQLException { + return 0 + } + + @Override + int getMaxColumnsInTable() throws SQLException { + return 0 + } + + @Override + int getMaxConnections() throws SQLException { + return 0 + } + + @Override + int getMaxCursorNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxIndexLength() throws SQLException { + return 0 + } + + @Override + int getMaxSchemaNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxProcedureNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxCatalogNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxRowSize() throws SQLException { + return 0 + } + + @Override + boolean doesMaxRowSizeIncludeBlobs() throws SQLException { + return false + } + + @Override + int getMaxStatementLength() throws SQLException { + return 0 + } + + @Override + int getMaxStatements() throws SQLException { + return 0 + } + + @Override + int getMaxTableNameLength() throws SQLException { + return 0 + } + + @Override + int getMaxTablesInSelect() throws SQLException { + return 0 + } + + @Override + int getMaxUserNameLength() throws SQLException { + return 0 + } + + @Override + int getDefaultTransactionIsolation() throws SQLException { + return 0 + } + + @Override + boolean supportsTransactions() throws SQLException { + return false + } + + @Override + boolean supportsTransactionIsolationLevel(int level) throws SQLException { + return false + } + + @Override + boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { + return false + } + + @Override + boolean supportsDataManipulationTransactionsOnly() throws SQLException { + return false + } + + @Override + boolean dataDefinitionCausesTransactionCommit() throws SQLException { + return false + } + + @Override + boolean dataDefinitionIgnoredInTransactions() throws SQLException { + return false + } + + @Override + ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) throws SQLException { + return null + } + + @Override + ResultSet getSchemas() throws SQLException { + return null + } + + @Override + ResultSet getCatalogs() throws SQLException { + return null + } + + @Override + ResultSet getTableTypes() throws SQLException { + return null + } + + @Override + ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException { + return null + } + + @Override + ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { + return null + } + + @Override + ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { + return null + } + + @Override + ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { + return null + } + + @Override + ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { + return null + } + + @Override + ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) throws SQLException { + return null + } + + @Override + ResultSet getTypeInfo() throws SQLException { + return null + } + + @Override + ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException { + return null + } + + @Override + boolean supportsResultSetType(int type) throws SQLException { + return false + } + + @Override + boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { + return false + } + + @Override + boolean ownUpdatesAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean ownDeletesAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean ownInsertsAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean othersUpdatesAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean othersDeletesAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean othersInsertsAreVisible(int type) throws SQLException { + return false + } + + @Override + boolean updatesAreDetected(int type) throws SQLException { + return false + } + + @Override + boolean deletesAreDetected(int type) throws SQLException { + return false + } + + @Override + boolean insertsAreDetected(int type) throws SQLException { + return false + } + + @Override + boolean supportsBatchUpdates() throws SQLException { + return false + } + + @Override + ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { + return null + } + + @Override + Connection getConnection() throws SQLException { + return null + } + + @Override + boolean supportsSavepoints() throws SQLException { + return false + } + + @Override + boolean supportsNamedParameters() throws SQLException { + return false + } + + @Override + boolean supportsMultipleOpenResults() throws SQLException { + return false + } + + @Override + boolean supportsGetGeneratedKeys() throws SQLException { + return false + } + + @Override + ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) throws SQLException { + return null + } + + @Override + boolean supportsResultSetHoldability(int holdability) throws SQLException { + return false + } + + @Override + int getResultSetHoldability() throws SQLException { + return 0 + } + + @Override + int getDatabaseMajorVersion() throws SQLException { + return 0 + } + + @Override + int getDatabaseMinorVersion() throws SQLException { + return 0 + } + + @Override + int getJDBCMajorVersion() throws SQLException { + return 0 + } + + @Override + int getJDBCMinorVersion() throws SQLException { + return 0 + } + + @Override + int getSQLStateType() throws SQLException { + return 0 + } + + @Override + boolean locatorsUpdateCopy() throws SQLException { + return false + } + + @Override + boolean supportsStatementPooling() throws SQLException { + return false + } + + @Override + RowIdLifetime getRowIdLifetime() throws SQLException { + return null + } + + @Override + ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { + return null + } + + @Override + boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { + return false + } + + @Override + boolean autoCommitFailureClosesAllResultSets() throws SQLException { + return false + } + + @Override + ResultSet getClientInfoProperties() throws SQLException { + return null + } + + @Override + ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) throws SQLException { + return null + } + + @Override + ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + return null + } + + @Override + boolean generatedKeyAlwaysReturned() throws SQLException { + return false + } + + @Override + def T unwrap(Class iface) throws SQLException { + return null + } + + @Override + boolean isWrapperFor(Class iface) throws SQLException { + return false + } +} diff --git a/java-agent/instrumentation/jdbc/src/test/groovy/test/TestDriver.groovy b/java-agent/instrumentation/jdbc/src/test/groovy/test/TestDriver.groovy new file mode 100644 index 0000000000..7983583ad2 --- /dev/null +++ b/java-agent/instrumentation/jdbc/src/test/groovy/test/TestDriver.groovy @@ -0,0 +1,45 @@ +package test + +import java.sql.Connection +import java.sql.Driver +import java.sql.DriverPropertyInfo +import java.sql.SQLException +import java.sql.SQLFeatureNotSupportedException +import java.util.logging.Logger + +class TestDriver implements Driver { + @Override + Connection connect(String url, Properties info) throws SQLException { + return new TestConnection("connectException=true" == url) + } + + @Override + boolean acceptsURL(String url) throws SQLException { + return false + } + + @Override + DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + return new DriverPropertyInfo[0] + } + + @Override + int getMajorVersion() { + return 0 + } + + @Override + int getMinorVersion() { + return 0 + } + + @Override + boolean jdbcCompliant() { + return false + } + + @Override + Logger getParentLogger() throws SQLFeatureNotSupportedException { + return null + } +} diff --git a/java-agent/instrumentation/jdbc/src/test/groovy/test/TestStatement.groovy b/java-agent/instrumentation/jdbc/src/test/groovy/test/TestStatement.groovy new file mode 100644 index 0000000000..4d87091a6a --- /dev/null +++ b/java-agent/instrumentation/jdbc/src/test/groovy/test/TestStatement.groovy @@ -0,0 +1,235 @@ +package test + +import java.sql.Connection +import java.sql.ResultSet +import java.sql.SQLException +import java.sql.SQLWarning +import java.sql.Statement + +class TestStatement implements Statement { + final Connection connection + + TestStatement(Connection connection) { + this.connection = connection + } + + @Override + ResultSet executeQuery(String sql) throws SQLException { + return null + } + + @Override + int executeUpdate(String sql) throws SQLException { + return 0 + } + + @Override + void close() throws SQLException { + + } + + @Override + int getMaxFieldSize() throws SQLException { + return 0 + } + + @Override + void setMaxFieldSize(int max) throws SQLException { + + } + + @Override + int getMaxRows() throws SQLException { + return 0 + } + + @Override + void setMaxRows(int max) throws SQLException { + + } + + @Override + void setEscapeProcessing(boolean enable) throws SQLException { + + } + + @Override + int getQueryTimeout() throws SQLException { + return 0 + } + + @Override + void setQueryTimeout(int seconds) throws SQLException { + + } + + @Override + void cancel() throws SQLException { + + } + + @Override + SQLWarning getWarnings() throws SQLException { + return null + } + + @Override + void clearWarnings() throws SQLException { + + } + + @Override + void setCursorName(String name) throws SQLException { + + } + + @Override + boolean execute(String sql) throws SQLException { + return false + } + + @Override + ResultSet getResultSet() throws SQLException { + return null + } + + @Override + int getUpdateCount() throws SQLException { + return 0 + } + + @Override + boolean getMoreResults() throws SQLException { + return false + } + + @Override + void setFetchDirection(int direction) throws SQLException { + + } + + @Override + int getFetchDirection() throws SQLException { + return 0 + } + + @Override + void setFetchSize(int rows) throws SQLException { + + } + + @Override + int getFetchSize() throws SQLException { + return 0 + } + + @Override + int getResultSetConcurrency() throws SQLException { + return 0 + } + + @Override + int getResultSetType() throws SQLException { + return 0 + } + + @Override + void addBatch(String sql) throws SQLException { + + } + + @Override + void clearBatch() throws SQLException { + + } + + @Override + int[] executeBatch() throws SQLException { + return new int[0] + } + + @Override + Connection getConnection() throws SQLException { + return connection + } + + @Override + boolean getMoreResults(int current) throws SQLException { + return false + } + + @Override + ResultSet getGeneratedKeys() throws SQLException { + return null + } + + @Override + int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + return 0 + } + + @Override + int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + return 0 + } + + @Override + int executeUpdate(String sql, String[] columnNames) throws SQLException { + return 0 + } + + @Override + boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + return false + } + + @Override + boolean execute(String sql, int[] columnIndexes) throws SQLException { + return false + } + + @Override + boolean execute(String sql, String[] columnNames) throws SQLException { + return false + } + + @Override + int getResultSetHoldability() throws SQLException { + return 0 + } + + @Override + boolean isClosed() throws SQLException { + return false + } + + @Override + void setPoolable(boolean poolable) throws SQLException { + + } + + @Override + boolean isPoolable() throws SQLException { + return false + } + + @Override + void closeOnCompletion() throws SQLException { + + } + + @Override + boolean isCloseOnCompletion() throws SQLException { + return false + } + + @Override + def T unwrap(Class iface) throws SQLException { + return null + } + + @Override + boolean isWrapperFor(Class iface) throws SQLException { + return false + } +} diff --git a/java-agent/instrumentation/jedis-1.4/jedis-1.4.gradle b/java-agent/instrumentation/jedis-1.4/jedis-1.4.gradle index 459a688e75..a3f451170e 100644 --- a/java-agent/instrumentation/jedis-1.4/jedis-1.4.gradle +++ b/java-agent/instrumentation/jedis-1.4/jedis-1.4.gradle @@ -3,8 +3,8 @@ muzzle { group = "redis.clients" module = "jedis" versions = "[1.4.0,3.0.0)" + assertInverse = true } - // Muzzle doesn't detect the classLoaderMatcher, so we can't assert fail for 3.0+ } apply from: "${rootDir}/gradle/java.gradle" diff --git a/java-agent/instrumentation/jsp-2.3/src/test/groovy/JSPInstrumentationBasicTests.groovy b/java-agent/instrumentation/jsp-2.3/src/test/groovy/JSPInstrumentationBasicTests.groovy index e825509b1f..a6b649ed34 100644 --- a/java-agent/instrumentation/jsp-2.3/src/test/groovy/JSPInstrumentationBasicTests.groovy +++ b/java-agent/instrumentation/jsp-2.3/src/test/groovy/JSPInstrumentationBasicTests.groovy @@ -584,4 +584,48 @@ class JSPInstrumentationBasicTests extends AgentTestRunner { "normal" | "compileError.jsp" | "compileError_jsp" | "" "forward" | "forwards/forwardWithCompileError.jsp" | "forwardWithCompileError_jsp" | "forwards." } + + def "direct static file reference"() { + setup: + String reqUrl = baseUrl + "/$staticFile" + def req = new Request.Builder().url(new URL(reqUrl)).get().build() + + when: + Response res = client.newCall(req).execute() + + then: + res.code() == HttpStatus.OK_200 + assertTraces(1) { + trace(0, 1) { + span(0) { + parent() + // serviceName jspWebappContext + operationName "servlet.request" + // FIXME: this is not a great resource name for serving static content. + // resourceName "GET /$jspWebappContext/$staticFile" + errored false + tags { + "$MoreTags.SPAN_TYPE" SpanTypes.HTTP_SERVER + "$Tags.COMPONENT" "java-web-servlet" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER + "$Tags.PEER_HOSTNAME" "127.0.0.1" + "$Tags.PEER_HOST_IPV4" "127.0.0.1" + "$Tags.PEER_PORT" Long + "$Tags.HTTP_URL" "http://localhost:$port/$jspWebappContext/$staticFile" + "$Tags.HTTP_METHOD" "GET" + "$Tags.HTTP_STATUS" 200 + "span.origin.type" "org.apache.catalina.core.ApplicationFilterChain" + "servlet.context" "/$jspWebappContext" + "servlet.path" "/$staticFile" + } + } + } + } + + cleanup: + res.close() + + where: + staticFile = "common/hello.html" + } } diff --git a/java-agent/instrumentation/rmi/src/main/java/io/opentelemetry/auto/instrumentation/rmi/client/RmiClientDecorator.java b/java-agent/instrumentation/rmi/src/main/java/io/opentelemetry/auto/instrumentation/rmi/client/RmiClientDecorator.java index a51f0d615a..8f6f684213 100644 --- a/java-agent/instrumentation/rmi/src/main/java/io/opentelemetry/auto/instrumentation/rmi/client/RmiClientDecorator.java +++ b/java-agent/instrumentation/rmi/src/main/java/io/opentelemetry/auto/instrumentation/rmi/client/RmiClientDecorator.java @@ -8,7 +8,7 @@ public class RmiClientDecorator extends ClientDecorator { @Override protected String[] instrumentationNames() { - return new String[] {"rmi"}; + return new String[] {"rmi", "rmi-client"}; } @Override @@ -23,6 +23,6 @@ public class RmiClientDecorator extends ClientDecorator { @Override protected String service() { - return "rmi"; + return null; } } diff --git a/java-agent/instrumentation/rmi/src/main/java/io/opentelemetry/auto/instrumentation/rmi/server/RmiServerDecorator.java b/java-agent/instrumentation/rmi/src/main/java/io/opentelemetry/auto/instrumentation/rmi/server/RmiServerDecorator.java index fbf0fb96e8..a8654285cc 100644 --- a/java-agent/instrumentation/rmi/src/main/java/io/opentelemetry/auto/instrumentation/rmi/server/RmiServerDecorator.java +++ b/java-agent/instrumentation/rmi/src/main/java/io/opentelemetry/auto/instrumentation/rmi/server/RmiServerDecorator.java @@ -8,7 +8,7 @@ public class RmiServerDecorator extends ServerDecorator { @Override protected String[] instrumentationNames() { - return new String[] {"rmi"}; + return new String[] {"rmi", "rmi-server"}; } @Override diff --git a/java-agent/instrumentation/rmi/src/test/groovy/RmiTest.groovy b/java-agent/instrumentation/rmi/src/test/groovy/RmiTest.groovy index a7db778bbe..6229785c28 100644 --- a/java-agent/instrumentation/rmi/src/test/groovy/RmiTest.groovy +++ b/java-agent/instrumentation/rmi/src/test/groovy/RmiTest.groovy @@ -43,7 +43,6 @@ class RmiTest extends AgentTestRunner { operationName "rmi.invoke" childOf span(0) tags { - "$MoreTags.SERVICE_NAME" "rmi" "$MoreTags.RESOURCE_NAME" "Greeter.hello" "$MoreTags.SPAN_TYPE" SpanTypes.RPC "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT @@ -118,7 +117,6 @@ class RmiTest extends AgentTestRunner { childOf span(0) errored true tags { - "$MoreTags.SERVICE_NAME" "rmi" "$MoreTags.RESOURCE_NAME" "Greeter.exceptional" "$MoreTags.SPAN_TYPE" SpanTypes.RPC "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT @@ -166,7 +164,6 @@ class RmiTest extends AgentTestRunner { operationName "rmi.invoke" childOf span(0) tags { - "$MoreTags.SERVICE_NAME" "rmi" "$MoreTags.RESOURCE_NAME" "Greeter.hello" "$MoreTags.SPAN_TYPE" SpanTypes.RPC "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT diff --git a/java-agent/instrumentation/servlet/request-2/request-2.gradle b/java-agent/instrumentation/servlet/request-2/request-2.gradle index 46ca27cdb7..d00f39d7cc 100644 --- a/java-agent/instrumentation/servlet/request-2/request-2.gradle +++ b/java-agent/instrumentation/servlet/request-2/request-2.gradle @@ -5,8 +5,12 @@ muzzle { versions = "[2.3, 3.0)" assertInverse = true } - // can't add a fail block for servlet 3, because servlet 3 is backward compatible with servlet 2.3+, - // meaning that for every class that exists in servlet 2, it also exists in servlet 3 + + fail { + group = "javax.servlet" + module = 'javax.servlet-api' + versions = "[3.0,)" + } } apply from: "${rootDir}/gradle/java.gradle" diff --git a/java-agent/instrumentation/servlet/request-2/src/main/java/io/opentelemetry/auto/instrumentation/servlet2/Servlet2Advice.java b/java-agent/instrumentation/servlet/request-2/src/main/java/io/opentelemetry/auto/instrumentation/servlet2/Servlet2Advice.java index 3c082f7bc1..e899485ef4 100644 --- a/java-agent/instrumentation/servlet/request-2/src/main/java/io/opentelemetry/auto/instrumentation/servlet2/Servlet2Advice.java +++ b/java-agent/instrumentation/servlet/request-2/src/main/java/io/opentelemetry/auto/instrumentation/servlet2/Servlet2Advice.java @@ -9,6 +9,7 @@ import static io.opentelemetry.auto.instrumentation.servlet2.HttpServletRequestE import static io.opentelemetry.auto.instrumentation.servlet2.Servlet2Decorator.DECORATE; import io.opentelemetry.auto.api.MoreTags; +import io.opentelemetry.auto.bootstrap.InstrumentationContext; import io.opentelemetry.auto.instrumentation.api.AgentScope; import io.opentelemetry.auto.instrumentation.api.AgentSpan; import io.opentelemetry.auto.instrumentation.api.Tags; @@ -36,12 +37,16 @@ public class Servlet2Advice { return null; } + final HttpServletRequest httpServletRequest = (HttpServletRequest) request; + if (response instanceof HttpServletResponse) { + // For use by HttpServletResponseInstrumentation: + InstrumentationContext.get(HttpServletResponse.class, HttpServletRequest.class) + .put((HttpServletResponse) response, httpServletRequest); + response = new StatusSavingHttpServletResponseWrapper((HttpServletResponse) response); } - final HttpServletRequest httpServletRequest = (HttpServletRequest) request; - final AgentSpan.Context extractedContext = propagate().extract(httpServletRequest, GETTER); final AgentSpan span = diff --git a/java-agent/instrumentation/servlet/request-2/src/main/java/io/opentelemetry/auto/instrumentation/servlet2/Servlet2Instrumentation.java b/java-agent/instrumentation/servlet/request-2/src/main/java/io/opentelemetry/auto/instrumentation/servlet2/Servlet2Instrumentation.java index 280156141b..6ebc1dfb80 100644 --- a/java-agent/instrumentation/servlet/request-2/src/main/java/io/opentelemetry/auto/instrumentation/servlet2/Servlet2Instrumentation.java +++ b/java-agent/instrumentation/servlet/request-2/src/main/java/io/opentelemetry/auto/instrumentation/servlet2/Servlet2Instrumentation.java @@ -49,6 +49,12 @@ public final class Servlet2Instrumentation extends Instrumenter.Default { named("javax.servlet.FilterChain").or(named("javax.servlet.http.HttpServlet")))); } + @Override + public Map contextStore() { + return singletonMap( + "javax.servlet.http.HttpServletResponse", "javax.servlet.http.HttpServletRequest"); + } + /** * Here we are instrumenting the public method for HttpServlet. This should ensure that this * advice is always called before HttpServletInstrumentation which is instrumenting the protected diff --git a/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/HttpServletRequestExtractAdapter.java b/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/HttpServletRequestExtractAdapter.java index f85e614898..6e5342252e 100644 --- a/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/HttpServletRequestExtractAdapter.java +++ b/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/HttpServletRequestExtractAdapter.java @@ -1,7 +1,6 @@ package io.opentelemetry.auto.instrumentation.servlet3; import io.opentelemetry.auto.instrumentation.api.AgentPropagation; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.servlet.http.HttpServletRequest; @@ -14,7 +13,7 @@ public class HttpServletRequestExtractAdapter @Override public List keys(final HttpServletRequest carrier) { - final ArrayList keys = Collections.list(carrier.getHeaderNames()); + final List keys = Collections.list(carrier.getHeaderNames()); keys.addAll(Collections.list(carrier.getAttributeNames())); return keys; } diff --git a/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/Servlet3Advice.java b/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/Servlet3Advice.java index e30d87c678..12985244eb 100644 --- a/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/Servlet3Advice.java +++ b/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/Servlet3Advice.java @@ -9,6 +9,7 @@ import static io.opentelemetry.auto.instrumentation.servlet3.HttpServletRequestE import static io.opentelemetry.auto.instrumentation.servlet3.Servlet3Decorator.DECORATE; import io.opentelemetry.auto.api.MoreTags; +import io.opentelemetry.auto.bootstrap.InstrumentationContext; import io.opentelemetry.auto.instrumentation.api.AgentScope; import io.opentelemetry.auto.instrumentation.api.AgentSpan; import io.opentelemetry.auto.instrumentation.api.Tags; @@ -24,7 +25,10 @@ public class Servlet3Advice { @Advice.OnMethodEnter(suppress = Throwable.class) public static AgentScope onEnter( - @Advice.This final Object servlet, @Advice.Argument(0) final ServletRequest request) { + @Advice.This final Object servlet, + @Advice.Argument(0) final ServletRequest request, + @Advice.Argument(1) final ServletResponse response) { + final boolean hasActiveTrace = activeSpan() != null; final boolean hasServletTrace = request.getAttribute(SPAN_ATTRIBUTE) instanceof AgentSpan; final boolean invalidRequest = !(request instanceof HttpServletRequest); @@ -35,6 +39,10 @@ public class Servlet3Advice { final HttpServletRequest httpServletRequest = (HttpServletRequest) request; + // For use by HttpServletResponseInstrumentation: + InstrumentationContext.get(HttpServletResponse.class, HttpServletRequest.class) + .put((HttpServletResponse) response, httpServletRequest); + final AgentSpan.Context extractedContext = propagate().extract(httpServletRequest, GETTER); final AgentSpan span = diff --git a/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/Servlet3Instrumentation.java b/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/Servlet3Instrumentation.java index f8c78b1156..0049d2d4ce 100644 --- a/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/Servlet3Instrumentation.java +++ b/java-agent/instrumentation/servlet/request-3/src/main/java/io/opentelemetry/auto/instrumentation/servlet3/Servlet3Instrumentation.java @@ -41,6 +41,12 @@ public final class Servlet3Instrumentation extends Instrumenter.Default { named("javax.servlet.FilterChain").or(named("javax.servlet.http.HttpServlet")))); } + @Override + public Map contextStore() { + return singletonMap( + "javax.servlet.http.HttpServletResponse", "javax.servlet.http.HttpServletRequest"); + } + /** * Here we are instrumenting the public method for HttpServlet. This should ensure that this * advice is always called before HttpServletInstrumentation which is instrumenting the protected diff --git a/java-agent/instrumentation/servlet/servlet.gradle b/java-agent/instrumentation/servlet/servlet.gradle index ff6901ae6f..49f7774d81 100644 --- a/java-agent/instrumentation/servlet/servlet.gradle +++ b/java-agent/instrumentation/servlet/servlet.gradle @@ -1 +1,29 @@ +muzzle { + pass { + group = "javax.servlet" + module = 'javax.servlet-api' + versions = "[,]" + assertInverse = true + } + pass { + group = "javax.servlet" + module = 'servlet-api' + versions = "[,]" + } +} + apply from: "${rootDir}/gradle/java.gradle" + +dependencies { + compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.3' + + testCompile group: 'javax.servlet', name: 'servlet-api', version: '2.3' + + // servlet request instrumentation required for linking request to response. + testCompile project(':java-agent:instrumentation:servlet:request-2') + + // Don't want to conflict with jetty from the test server. + testCompile(project(':java-agent:testing')) { + exclude group: 'org.eclipse.jetty', module: 'jetty-server' + } +} diff --git a/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/ServletRequestSetter.java b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/ServletRequestSetter.java new file mode 100644 index 0000000000..7f9f07b9bb --- /dev/null +++ b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/ServletRequestSetter.java @@ -0,0 +1,14 @@ +package io.opentelemetry.auto.instrumentation.servlet; + +import io.opentelemetry.auto.instrumentation.api.AgentPropagation; +import javax.servlet.ServletRequest; + +/** Inject into request attributes since the request headers can't be modified. */ +public class ServletRequestSetter implements AgentPropagation.Setter { + public static final ServletRequestSetter SETTER = new ServletRequestSetter(); + + @Override + public void set(final ServletRequest carrier, final String key, final String value) { + carrier.setAttribute(key, value); + } +} diff --git a/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/dispatcher/RequestDispatcherDecorator.java b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/dispatcher/RequestDispatcherDecorator.java new file mode 100644 index 0000000000..410d1cbc9c --- /dev/null +++ b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/dispatcher/RequestDispatcherDecorator.java @@ -0,0 +1,22 @@ +package io.opentelemetry.auto.instrumentation.servlet.dispatcher; + +import io.opentelemetry.auto.decorator.BaseDecorator; + +public class RequestDispatcherDecorator extends BaseDecorator { + public static final RequestDispatcherDecorator DECORATE = new RequestDispatcherDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"servlet", "servlet-dispatcher"}; + } + + @Override + protected String spanType() { + return null; + } + + @Override + protected String component() { + return "java-web-servlet-dispatcher"; + } +} diff --git a/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/dispatcher/RequestDispatcherInstrumentation.java b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/dispatcher/RequestDispatcherInstrumentation.java new file mode 100644 index 0000000000..845e2c077d --- /dev/null +++ b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/dispatcher/RequestDispatcherInstrumentation.java @@ -0,0 +1,113 @@ +package io.opentelemetry.auto.instrumentation.servlet.dispatcher; + +import static io.opentelemetry.auto.decorator.HttpServerDecorator.SPAN_ATTRIBUTE; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.activateSpan; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.activeSpan; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.propagate; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.startSpan; +import static io.opentelemetry.auto.instrumentation.servlet.ServletRequestSetter.SETTER; +import static io.opentelemetry.auto.instrumentation.servlet.dispatcher.RequestDispatcherDecorator.DECORATE; +import static io.opentelemetry.auto.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import io.opentelemetry.auto.api.MoreTags; +import io.opentelemetry.auto.bootstrap.InstrumentationContext; +import io.opentelemetry.auto.instrumentation.api.AgentScope; +import io.opentelemetry.auto.instrumentation.api.AgentSpan; +import io.opentelemetry.auto.tooling.Instrumenter; +import java.util.Map; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletRequest; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class RequestDispatcherInstrumentation extends Instrumenter.Default { + public RequestDispatcherInstrumentation() { + super("servlet", "servlet-dispatcher"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + "io.opentelemetry.auto.instrumentation.servlet.ServletRequestSetter", + "io.opentelemetry.auto.decorator.BaseDecorator", + packageName + ".RequestDispatcherDecorator", + }; + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()).and(safeHasSuperType(named("javax.servlet.RequestDispatcher"))); + } + + @Override + public Map contextStore() { + return singletonMap("javax.servlet.RequestDispatcher", String.class.getName()); + } + + @Override + public Map, String> transformers() { + return singletonMap( + named("forward") + .or(named("include")) + .and(takesArgument(0, named("javax.servlet.ServletRequest"))) + .and(takesArgument(1, named("javax.servlet.ServletResponse"))) + .and(isPublic()), + RequestDispatcherAdvice.class.getName()); + } + + public static class RequestDispatcherAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope start( + @Advice.Origin("#m") final String method, + @Advice.This final RequestDispatcher dispatcher, + @Advice.Argument(0) final ServletRequest request) { + if (activeSpan() == null) { + // Don't want to generate a new top-level span + return null; + } + + final AgentSpan span = startSpan("servlet." + method); + DECORATE.afterStart(span); + + final String target = + InstrumentationContext.get(RequestDispatcher.class, String.class).get(dispatcher); + span.setAttribute(MoreTags.RESOURCE_NAME, target); + + // In case we lose context, inject trace into to the request. + propagate().inject(span, request, SETTER); + + // temporarily remove from request to avoid spring resource name bubbling up: + request.removeAttribute(SPAN_ATTRIBUTE); + + return activateSpan(span, true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stop( + @Advice.Enter final AgentScope scope, + @Advice.Argument(0) final ServletRequest request, + @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + + // now add it back... + request.setAttribute(SPAN_ATTRIBUTE, scope.span()); + + DECORATE.onError(scope, throwable); + DECORATE.beforeFinish(scope); + scope.close(); + } + } +} diff --git a/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java new file mode 100644 index 0000000000..62c3b4d16c --- /dev/null +++ b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/dispatcher/ServletContextInstrumentation.java @@ -0,0 +1,57 @@ +package io.opentelemetry.auto.instrumentation.servlet.dispatcher; + +import static io.opentelemetry.auto.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import io.opentelemetry.auto.bootstrap.InstrumentationContext; +import io.opentelemetry.auto.tooling.Instrumenter; +import java.util.Map; +import javax.servlet.RequestDispatcher; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class ServletContextInstrumentation extends Instrumenter.Default { + public ServletContextInstrumentation() { + super("servlet", "servlet-dispatcher"); + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()).and(safeHasSuperType(named("javax.servlet.ServletContext"))); + } + + @Override + public Map contextStore() { + return singletonMap("javax.servlet.RequestDispatcher", String.class.getName()); + } + + @Override + public Map, String> transformers() { + return singletonMap( + returns(named("javax.servlet.RequestDispatcher")) + .and(takesArgument(0, String.class)) + // javax.servlet.ServletContext.getRequestDispatcher + // javax.servlet.ServletContext.getNamedDispatcher + .and(isPublic()), + RequestDispatcherTargetAdvice.class.getName()); + } + + public static class RequestDispatcherTargetAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void saveTarget( + @Advice.Argument(0) final String target, + @Advice.Return final RequestDispatcher dispatcher) { + InstrumentationContext.get(RequestDispatcher.class, String.class).put(dispatcher, target); + } + } +} diff --git a/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/filter/FilterDecorator.java b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/filter/FilterDecorator.java new file mode 100644 index 0000000000..ec1a813159 --- /dev/null +++ b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/filter/FilterDecorator.java @@ -0,0 +1,22 @@ +package io.opentelemetry.auto.instrumentation.servlet.filter; + +import io.opentelemetry.auto.decorator.BaseDecorator; + +public class FilterDecorator extends BaseDecorator { + public static final FilterDecorator DECORATE = new FilterDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"servlet-filter"}; + } + + @Override + protected String spanType() { + return null; + } + + @Override + protected String component() { + return "java-web-servlet-filter"; + } +} diff --git a/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/filter/FilterInstrumentation.java b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/filter/FilterInstrumentation.java new file mode 100644 index 0000000000..dd4d313851 --- /dev/null +++ b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/filter/FilterInstrumentation.java @@ -0,0 +1,88 @@ +package io.opentelemetry.auto.instrumentation.servlet.filter; + +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.activateSpan; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.activeSpan; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.startSpan; +import static io.opentelemetry.auto.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import io.opentelemetry.auto.api.MoreTags; +import io.opentelemetry.auto.instrumentation.api.AgentScope; +import io.opentelemetry.auto.instrumentation.api.AgentSpan; +import io.opentelemetry.auto.tooling.Instrumenter; +import java.util.Map; +import javax.servlet.Filter; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class FilterInstrumentation extends Instrumenter.Default { + public FilterInstrumentation() { + super("servlet-filter"); + } + + @Override + public boolean defaultEnabled() { + return false; + } + + @Override + public String[] helperClassNames() { + return new String[] { + "io.opentelemetry.auto.decorator.BaseDecorator", packageName + ".FilterDecorator", + }; + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()).and(safeHasSuperType(named("javax.servlet.Filter"))); + } + + @Override + public Map, String> transformers() { + return singletonMap( + named("doFilter") + .and(takesArgument(0, named("javax.servlet.ServletRequest"))) + .and(takesArgument(1, named("javax.servlet.ServletResponse"))) + .and(isPublic()), + FilterAdvice.class.getName()); + } + + public static class FilterAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope start(@Advice.This final Filter filter) { + if (activeSpan() == null) { + // Don't want to generate a new top-level span + return null; + } + + final AgentSpan span = startSpan("servlet.filter"); + FilterDecorator.DECORATE.afterStart(span); + + // Here we use "this" instead of "the method target" to distinguish abstract filter instances. + span.setAttribute(MoreTags.RESOURCE_NAME, filter.getClass().getSimpleName() + ".doFilter"); + + return activateSpan(span, true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + FilterDecorator.DECORATE.onError(scope, throwable); + FilterDecorator.DECORATE.beforeFinish(scope); + scope.close(); + } + } +} diff --git a/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletDecorator.java b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletDecorator.java new file mode 100644 index 0000000000..677b51ea35 --- /dev/null +++ b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletDecorator.java @@ -0,0 +1,22 @@ +package io.opentelemetry.auto.instrumentation.servlet.http; + +import io.opentelemetry.auto.decorator.BaseDecorator; + +public class HttpServletDecorator extends BaseDecorator { + public static final HttpServletDecorator DECORATE = new HttpServletDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"servlet-service"}; + } + + @Override + protected String spanType() { + return null; + } + + @Override + protected String component() { + return "java-web-servlet-service"; + } +} diff --git a/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletInstrumentation.java b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletInstrumentation.java new file mode 100644 index 0000000000..6900052345 --- /dev/null +++ b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletInstrumentation.java @@ -0,0 +1,97 @@ +package io.opentelemetry.auto.instrumentation.servlet.http; + +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.activateSpan; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.activeSpan; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.startSpan; +import static io.opentelemetry.auto.instrumentation.servlet.http.HttpServletDecorator.DECORATE; +import static io.opentelemetry.auto.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isProtected; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +import com.google.auto.service.AutoService; +import io.opentelemetry.auto.api.MoreTags; +import io.opentelemetry.auto.instrumentation.api.AgentScope; +import io.opentelemetry.auto.instrumentation.api.AgentSpan; +import io.opentelemetry.auto.tooling.Instrumenter; +import java.lang.reflect.Method; +import java.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class HttpServletInstrumentation extends Instrumenter.Default { + public HttpServletInstrumentation() { + super("servlet-service"); + } + + @Override + public boolean defaultEnabled() { + return false; + } + + @Override + public String[] helperClassNames() { + return new String[] { + "io.opentelemetry.auto.decorator.BaseDecorator", packageName + ".HttpServletDecorator", + }; + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()).and(safeHasSuperType(named("javax.servlet.http.HttpServlet"))); + } + + /** + * Here we are instrumenting the protected method for HttpServlet. This should ensure that this + * advice is always called after Servlet3Instrumentation which is instrumenting the public method. + */ + @Override + public Map, String> transformers() { + return singletonMap( + named("service") + .or(nameStartsWith("do")) // doGet, doPost, etc + .and(takesArgument(0, named("javax.servlet.http.HttpServletRequest"))) + .and(takesArgument(1, named("javax.servlet.http.HttpServletResponse"))) + .and(isProtected().or(isPublic())), + HttpServletAdvice.class.getName()); + } + + public static class HttpServletAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope start(@Advice.Origin final Method method) { + + if (activeSpan() == null) { + // Don't want to generate a new top-level span + return null; + } + + final AgentSpan span = startSpan("servlet." + method.getName()); + DECORATE.afterStart(span); + + // Here we use the Method instead of "this.class.name" to distinguish calls to "super". + span.setAttribute(MoreTags.RESOURCE_NAME, DECORATE.spanNameForMethod(method)); + + return activateSpan(span, true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + DECORATE.onError(scope, throwable); + DECORATE.beforeFinish(scope); + scope.close(); + } + } +} diff --git a/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletResponseDecorator.java b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletResponseDecorator.java new file mode 100644 index 0000000000..e24595c93d --- /dev/null +++ b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletResponseDecorator.java @@ -0,0 +1,22 @@ +package io.opentelemetry.auto.instrumentation.servlet.http; + +import io.opentelemetry.auto.decorator.BaseDecorator; + +public class HttpServletResponseDecorator extends BaseDecorator { + public static final HttpServletResponseDecorator DECORATE = new HttpServletResponseDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"servlet", "servlet-response"}; + } + + @Override + protected String spanType() { + return null; + } + + @Override + protected String component() { + return "java-web-servlet-response"; + } +} diff --git a/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletResponseInstrumentation.java b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletResponseInstrumentation.java new file mode 100644 index 0000000000..362af11b12 --- /dev/null +++ b/java-agent/instrumentation/servlet/src/main/java/io/opentelemetry/auto/instrumentation/servlet/http/HttpServletResponseInstrumentation.java @@ -0,0 +1,99 @@ +package io.opentelemetry.auto.instrumentation.servlet.http; + +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.activateSpan; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.activeSpan; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.propagate; +import static io.opentelemetry.auto.instrumentation.api.AgentTracer.startSpan; +import static io.opentelemetry.auto.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import io.opentelemetry.auto.api.MoreTags; +import io.opentelemetry.auto.bootstrap.InstrumentationContext; +import io.opentelemetry.auto.instrumentation.api.AgentScope; +import io.opentelemetry.auto.instrumentation.api.AgentSpan; +import io.opentelemetry.auto.instrumentation.servlet.ServletRequestSetter; +import io.opentelemetry.auto.tooling.Instrumenter; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(Instrumenter.class) +public final class HttpServletResponseInstrumentation extends Instrumenter.Default { + public HttpServletResponseInstrumentation() { + super("servlet", "servlet-response"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + "io.opentelemetry.auto.instrumentation.servlet.ServletRequestSetter", + "io.opentelemetry.auto.decorator.BaseDecorator", + packageName + ".HttpServletResponseDecorator", + }; + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()) + .and(safeHasSuperType(named("javax.servlet.http.HttpServletResponse"))); + } + + @Override + public Map, String> transformers() { + return singletonMap(named("sendError").or(named("sendRedirect")), SendAdvice.class.getName()); + } + + @Override + public Map contextStore() { + return singletonMap( + "javax.servlet.http.HttpServletResponse", "javax.servlet.http.HttpServletRequest"); + } + + public static class SendAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope start( + @Advice.Origin("#m") final String method, @Advice.This final HttpServletResponse resp) { + if (activeSpan() == null) { + // Don't want to generate a new top-level span + return null; + } + + final HttpServletRequest req = + InstrumentationContext.get(HttpServletResponse.class, HttpServletRequest.class).get(resp); + if (req == null) { + // Missing the response->request linking... probably in a wrapped instance. + return null; + } + + final AgentSpan span = startSpan("servlet.response"); + HttpServletResponseDecorator.DECORATE.afterStart(span); + + span.setAttribute(MoreTags.RESOURCE_NAME, "HttpServletResponse." + method); + + // In case we lose context, inject trace into to the request. + propagate().inject(span, req, ServletRequestSetter.SETTER); + + return activateSpan(span, true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + HttpServletResponseDecorator.DECORATE.onError(scope, throwable); + HttpServletResponseDecorator.DECORATE.beforeFinish(scope); + scope.close(); + } + } +} diff --git a/java-agent/instrumentation/servlet/src/test/groovy/FilterTest.groovy b/java-agent/instrumentation/servlet/src/test/groovy/FilterTest.groovy new file mode 100644 index 0000000000..a157b097fd --- /dev/null +++ b/java-agent/instrumentation/servlet/src/test/groovy/FilterTest.groovy @@ -0,0 +1,106 @@ +import io.opentelemetry.auto.api.MoreTags +import io.opentelemetry.auto.instrumentation.api.Tags +import io.opentelemetry.auto.test.AgentTestRunner + +import javax.servlet.Filter +import javax.servlet.FilterChain +import javax.servlet.FilterConfig +import javax.servlet.ServletException +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse + +import static io.opentelemetry.auto.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.auto.test.utils.TraceUtils.runUnderTrace + +class FilterTest extends AgentTestRunner { + static { + System.setProperty("opentelemetry.auto.integration.servlet-filter.enabled", "true") + } + + def "test doFilter no-parent"() { + when: + filter.doFilter(null, null, null) + + then: + assertTraces(0) {} + + where: + filter = new TestFilter() + } + + def "test doFilter with parent"() { + when: + runUnderTrace("parent") { + filter.doFilter(null, null, null) + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + operationName "servlet.filter" + childOf span(0) + tags { + "$MoreTags.RESOURCE_NAME" "${filter.class.simpleName}.doFilter" + "$Tags.COMPONENT" "java-web-servlet-filter" + } + } + } + } + + where: + filter << [new TestFilter(), new TestFilter() {}] + } + + def "test doFilter exception"() { + setup: + def ex = new Exception("some error") + def filter = new TestFilter() { + @Override + void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) { + throw ex + } + } + + when: + runUnderTrace("parent") { + filter.doFilter(null, null, null) + } + + then: + def th = thrown(Exception) + th == ex + + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, null, ex) + span(1) { + operationName "servlet.filter" + childOf span(0) + errored true + tags { + "$MoreTags.RESOURCE_NAME" "${filter.class.simpleName}.doFilter" + "$Tags.COMPONENT" "java-web-servlet-filter" + errorTags(ex.class, ex.message) + } + } + } + } + } + + static class TestFilter implements Filter { + + @Override + void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + } + + @Override + void destroy() { + } + } +} diff --git a/java-agent/instrumentation/servlet/src/test/groovy/HttpServletResponseTest.groovy b/java-agent/instrumentation/servlet/src/test/groovy/HttpServletResponseTest.groovy new file mode 100644 index 0000000000..6f891e024f --- /dev/null +++ b/java-agent/instrumentation/servlet/src/test/groovy/HttpServletResponseTest.groovy @@ -0,0 +1,280 @@ +import groovy.servlet.AbstractHttpServlet +import io.opentelemetry.auto.api.MoreTags +import io.opentelemetry.auto.instrumentation.api.Tags +import io.opentelemetry.auto.test.AgentTestRunner +import spock.lang.Subject + +import javax.servlet.ServletOutputStream +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse +import javax.servlet.http.Cookie +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import static io.opentelemetry.auto.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.auto.test.utils.TraceUtils.runUnderTrace +import static java.util.Collections.emptyEnumeration + +class HttpServletResponseTest extends AgentTestRunner { + + @Subject + def response = new TestResponse() + def request = Mock(HttpServletRequest) { + getMethod() >> "GET" + getProtocol() >> "TEST" + getHeaderNames() >> emptyEnumeration() + getAttributeNames() >> emptyEnumeration() + } + + def setup() { + def servlet = new AbstractHttpServlet() {} + // We need to call service so HttpServletAdvice can link the request to the response. + servlet.service((ServletRequest) request, (ServletResponse) response) + assert response.__opentelemetryContext$javax$servlet$http$HttpServletResponse != null + TEST_WRITER.clear() + } + + def "test send no-parent"() { + when: + response.sendError(0) + response.sendError(0, "") + response.sendRedirect("") + + then: + assertTraces(0) {} + } + + def "test send with parent"() { + when: + runUnderTrace("parent") { + response.sendError(0) + response.sendError(0, "") + response.sendRedirect("") + } + + then: + assertTraces(1) { + trace(0, 4) { + basicSpan(it, 0, "parent") + span(1) { + operationName "servlet.response" + childOf span(0) + tags { + "$MoreTags.RESOURCE_NAME" "HttpServletResponse.sendError" + "$Tags.COMPONENT" "java-web-servlet-response" + } + } + span(2) { + operationName "servlet.response" + childOf span(0) + tags { + "$MoreTags.RESOURCE_NAME" "HttpServletResponse.sendError" + "$Tags.COMPONENT" "java-web-servlet-response" + } + } + span(3) { + operationName "servlet.response" + childOf span(0) + tags { + "$MoreTags.RESOURCE_NAME" "HttpServletResponse.sendRedirect" + "$Tags.COMPONENT" "java-web-servlet-response" + } + } + } + } + } + + def "test send with exception"() { + setup: + def ex = new Exception("some error") + def response = new TestResponse() { + @Override + void sendRedirect(String s) { + throw ex + } + } + def servlet = new AbstractHttpServlet() {} + // We need to call service so HttpServletAdvice can link the request to the response. + servlet.service((ServletRequest) request, (ServletResponse) response) + assert response.__opentelemetryContext$javax$servlet$http$HttpServletResponse != null + TEST_WRITER.clear() + + when: + runUnderTrace("parent") { + response.sendRedirect("") + } + + then: + def th = thrown(Exception) + th == ex + + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, null, ex) + span(1) { + operationName "servlet.response" + childOf span(0) + errored true + tags { + "$MoreTags.RESOURCE_NAME" "HttpServletResponse.sendRedirect" + "$Tags.COMPONENT" "java-web-servlet-response" + errorTags(ex.class, ex.message) + } + } + } + } + } + + static class TestResponse implements HttpServletResponse { + + @Override + void addCookie(Cookie cookie) { + + } + + @Override + boolean containsHeader(String s) { + return false + } + + @Override + String encodeURL(String s) { + return null + } + + @Override + String encodeRedirectURL(String s) { + return null + } + + @Override + String encodeUrl(String s) { + return null + } + + @Override + String encodeRedirectUrl(String s) { + return null + } + + @Override + void sendError(int i, String s) throws IOException { + + } + + @Override + void sendError(int i) throws IOException { + + } + + @Override + void sendRedirect(String s) throws IOException { + + } + + @Override + void setDateHeader(String s, long l) { + + } + + @Override + void addDateHeader(String s, long l) { + + } + + @Override + void setHeader(String s, String s1) { + + } + + @Override + void addHeader(String s, String s1) { + + } + + @Override + void setIntHeader(String s, int i) { + + } + + @Override + void addIntHeader(String s, int i) { + + } + + @Override + void setStatus(int i) { + + } + + @Override + void setStatus(int i, String s) { + + } + + @Override + String getCharacterEncoding() { + return null + } + + @Override + ServletOutputStream getOutputStream() throws IOException { + return null + } + + @Override + PrintWriter getWriter() throws IOException { + return null + } + + @Override + void setContentLength(int i) { + + } + + @Override + void setContentType(String s) { + + } + + @Override + void setBufferSize(int i) { + + } + + @Override + int getBufferSize() { + return 0 + } + + @Override + void flushBuffer() throws IOException { + + } + + @Override + void resetBuffer() { + + } + + @Override + boolean isCommitted() { + return false + } + + @Override + void reset() { + + } + + @Override + void setLocale(Locale locale) { + + } + + @Override + Locale getLocale() { + return null + } + } +} diff --git a/java-agent/instrumentation/servlet/src/test/groovy/HttpServletTest.groovy b/java-agent/instrumentation/servlet/src/test/groovy/HttpServletTest.groovy new file mode 100644 index 0000000000..47cd5d238a --- /dev/null +++ b/java-agent/instrumentation/servlet/src/test/groovy/HttpServletTest.groovy @@ -0,0 +1,124 @@ +import groovy.servlet.AbstractHttpServlet +import io.opentelemetry.auto.api.MoreTags +import io.opentelemetry.auto.instrumentation.api.Tags +import io.opentelemetry.auto.test.AgentTestRunner + +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import static io.opentelemetry.auto.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.auto.test.utils.TraceUtils.runUnderTrace + +class HttpServletTest extends AgentTestRunner { + static { + System.setProperty("opentelemetry.auto.integration.servlet-service.enabled", "true") + } + + def req = Mock(HttpServletRequest) { + getMethod() >> "GET" + getProtocol() >> "TEST" + } + def resp = Mock(HttpServletResponse) + + def "test service no-parent"() { + when: + servlet.service(req, resp) + + then: + assertTraces(0) {} + + where: + servlet = new TestServlet() + } + + def "test service with parent"() { + when: + runUnderTrace("parent") { + servlet.service(req, resp) + } + + then: + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent") + span(1) { + operationName "servlet.service" + childOf span(0) + tags { + "$MoreTags.RESOURCE_NAME" "HttpServlet.service" + "$Tags.COMPONENT" "java-web-servlet-service" + } + } + span(2) { + operationName "servlet.doGet" + childOf span(1) + tags { + "$MoreTags.RESOURCE_NAME" "${expectedResourceName}.doGet" + "$Tags.COMPONENT" "java-web-servlet-service" + } + } + } + } + + where: + servlet << [new TestServlet(), new TestServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + } + }] + + expectedResourceName = servlet.class.anonymousClass ? servlet.class.name : servlet.class.simpleName + } + + def "test service exception"() { + setup: + def ex = new Exception("some error") + def servlet = new TestServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + throw ex + } + } + + when: + runUnderTrace("parent") { + servlet.service(req, resp) + } + + then: + def th = thrown(Exception) + th == ex + + assertTraces(1) { + trace(0, 3) { + basicSpan(it, 0, "parent", null, null, ex) + span(1) { + operationName "servlet.service" + childOf span(0) + errored true + tags { + "$MoreTags.RESOURCE_NAME" "HttpServlet.service" + "$Tags.COMPONENT" "java-web-servlet-service" + errorTags(ex.class, ex.message) + } + } + span(2) { + operationName "servlet.doGet" + childOf span(1) + errored true + tags { + "$MoreTags.RESOURCE_NAME" "${servlet.class.name}.doGet" + "$Tags.COMPONENT" "java-web-servlet-service" + errorTags(ex.class, ex.message) + } + } + } + } + } + + static class TestServlet extends AbstractHttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + } + } +} diff --git a/java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherTest.groovy b/java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherTest.groovy new file mode 100644 index 0000000000..fadcfffbbc --- /dev/null +++ b/java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherTest.groovy @@ -0,0 +1,95 @@ +import io.opentelemetry.auto.api.MoreTags +import io.opentelemetry.auto.instrumentation.api.Tags +import io.opentelemetry.auto.test.AgentTestRunner + +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import static io.opentelemetry.auto.test.utils.TraceUtils.basicSpan +import static io.opentelemetry.auto.test.utils.TraceUtils.runUnderTrace + +class RequestDispatcherTest extends AgentTestRunner { + + def dispatcher = new RequestDispatcherUtils(Mock(HttpServletRequest), Mock(HttpServletResponse)) + + def "test dispatch no-parent"() { + when: + dispatcher.forward("") + dispatcher.include("") + + then: + assertTraces(0) {} + } + + def "test dispatcher #method with parent"() { + when: + runUnderTrace("parent") { + dispatcher."$method"(target) + } + + then: + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent") + span(1) { + operationName "servlet.$operation" + childOf span(0) + tags { + "$MoreTags.RESOURCE_NAME" target + "$Tags.COMPONENT" "java-web-servlet-dispatcher" + } + } + } + } + + where: + operation | method + "forward" | "forward" + "forward" | "forwardNamed" + "include" | "include" + "include" | "includeNamed" + + target = "test-$method" + } + + def "test dispatcher #method exception"() { + setup: + def ex = new ServletException("some error") + def dispatcher = new RequestDispatcherUtils(Mock(HttpServletRequest), Mock(HttpServletResponse), ex) + + when: + runUnderTrace("parent") { + dispatcher."$method"(target) + } + + then: + def th = thrown(ServletException) + th == ex + + assertTraces(1) { + trace(0, 2) { + basicSpan(it, 0, "parent", null, null, ex) + span(1) { + operationName "servlet.$operation" + childOf span(0) + errored true + tags { + "$MoreTags.RESOURCE_NAME" target + "$Tags.COMPONENT" "java-web-servlet-dispatcher" + errorTags(ex.class, ex.message) + } + } + } + } + + where: + operation | method + "forward" | "forward" + "forward" | "forwardNamed" + "include" | "include" + "include" | "includeNamed" + + target = "test-$method" + } +} diff --git a/java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherUtils.java b/java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherUtils.java new file mode 100644 index 0000000000..2f08ad74ed --- /dev/null +++ b/java-agent/instrumentation/servlet/src/test/groovy/RequestDispatcherUtils.java @@ -0,0 +1,181 @@ +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.Set; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +public class RequestDispatcherUtils { + private final ServletRequest req; + private final ServletResponse resp; + private final ServletException toThrow; + + public RequestDispatcherUtils(final ServletRequest req, final ServletResponse resp) { + this.req = req; + this.resp = resp; + toThrow = null; + } + + public RequestDispatcherUtils( + final ServletRequest req, final ServletResponse resp, final ServletException toThrow) { + this.req = req; + this.resp = resp; + this.toThrow = toThrow; + } + + /* RequestDispatcher can't be visible to groovy otherwise things break, so everything is + * encapsulated in here where groovy doesn't need to access it. + */ + + void forward(final String target) throws ServletException, IOException { + new TestContext().getRequestDispatcher(target).forward(req, resp); + } + + void include(final String target) throws ServletException, IOException { + new TestContext().getRequestDispatcher(target).include(req, resp); + } + + void forwardNamed(final String target) throws ServletException, IOException { + new TestContext().getNamedDispatcher(target).forward(req, resp); + } + + void includeNamed(final String target) throws ServletException, IOException { + new TestContext().getNamedDispatcher(target).include(req, resp); + } + + class TestContext implements ServletContext { + @Override + public ServletContext getContext(final String s) { + return null; + } + + @Override + public int getMajorVersion() { + return 0; + } + + @Override + public int getMinorVersion() { + return 0; + } + + @Override + public String getMimeType(final String s) { + return null; + } + + @Override + public Set getResourcePaths(final String s) { + return null; + } + + @Override + public URL getResource(final String s) throws MalformedURLException { + return null; + } + + @Override + public InputStream getResourceAsStream(final String s) { + return null; + } + + @Override + public RequestDispatcher getRequestDispatcher(final String s) { + return new TestDispatcher(); + } + + @Override + public RequestDispatcher getNamedDispatcher(final String s) { + return new TestDispatcher(); + } + + @Override + public Servlet getServlet(final String s) throws ServletException { + return null; + } + + @Override + public Enumeration getServlets() { + return null; + } + + @Override + public Enumeration getServletNames() { + return null; + } + + @Override + public void log(final String s) {} + + @Override + public void log(final Exception e, final String s) {} + + @Override + public void log(final String s, final Throwable throwable) {} + + @Override + public String getRealPath(final String s) { + return null; + } + + @Override + public String getServerInfo() { + return null; + } + + @Override + public String getInitParameter(final String s) { + return null; + } + + @Override + public Enumeration getInitParameterNames() { + return null; + } + + @Override + public Object getAttribute(final String s) { + return null; + } + + @Override + public Enumeration getAttributeNames() { + return null; + } + + @Override + public void setAttribute(final String s, final Object o) {} + + @Override + public void removeAttribute(final String s) {} + + @Override + public String getServletContextName() { + return null; + } + } + + class TestDispatcher implements RequestDispatcher { + @Override + public void forward(final ServletRequest servletRequest, final ServletResponse servletResponse) + throws ServletException, IOException { + if (toThrow != null) { + throw toThrow; + } + } + + @Override + public void include(final ServletRequest servletRequest, final ServletResponse servletResponse) + throws ServletException, IOException { + if (toThrow != null) { + throw toThrow; + } + } + } +}