Docs for writing new instrumentation... (#652)

* Docs for writing new instrumentation
This commit is contained in:
Anuraag Agrawal 2020-07-19 14:21:19 +09:00 committed by GitHub
parent f3421e91a0
commit d8355c672d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 564 additions and 501 deletions

View File

@ -74,6 +74,126 @@ following "clusters" of classes:
- `io/opentelemetry/auto/shaded/` - contains OpenTelemetry API and its dependencies.
Shaded during creation of `javaagent` jar file by Shadow Gradle plugin.
### Writing instrumentation
**Warning**: The repository is still in the process of migrating to the structure described here.
Any time we want to add OpenTelemetry support for a new Java library, e.g., so usage
of that library has tracing, we must write new instrumentation for that library. Let's
go over some terms first.
**Manual Instrumentation**: This is logic that creates spans and enriches them with data
using library-specific monitoring APIs. For example, when instrumenting an RPC library,
the instrumentation will use some library-specific functionality to listen to events such
as the start and end of a request and will execute code to start and end spans in these
listeners. Many of these libraries will provide interception type APIs such as the gRPC
`ClientInterceptor` or servlet's `Filter`. Others will provide a Java interface whose methods
correspond to a request, and instrumentation can define an implementation which delegates
to the standard, wrapping methods with the logic to manage spans. Users will add code to their
apps that initialize the classes provided by manual instrumentation libraries and the libraries
can be found inside the user's app itself.
Some libraries will have no way of intercepting requests because they only expose static APIs
and no interception hooks. For these libraries it is not possible to create manual
instrumentation.
**Auto Instrumentation**: This is logic that is similar to manual instrumentation, but instead
of a user initializing classes themselves, a Java agent automatically initializes them during
class loading by manipulating byte code. This allows a user to develop their apps without thinking
about instrumentation and get it "for free". Often, the auto instrumentation will generate bytecode
that is more or less identical to what a user would have written themselves in their app.
In addition to automatically initializing manual instrumentation, auto instrumentation can be used
for libraries where manual instrumentation is not possible, such as `URLConnection`, because it can
intercept even the JDK's classes. Such libraries will not have manual instrumentation but will have
auto instrumentation.
#### Folder Structure
Please also refer to some of our existing instrumentation for examples of our structure, for example,
[aws-sdk-2.2](./instrumentation/aws-sdk/aws-sdk-2.2).
When writing new instrumentation, create a new subfolder of `instrumentation` to correspond to the
instrumented library and the oldest version being targeted. Ideally an old version of the library is
targeted in a way that the instrumentation applies to a large range of versions, but this may be
restricted by the interception APIs provided by the library.
Within the subfolder, create three folders `library` (skip if manual instrumentation is not possible),
`auto`, and `testing`.
For example, if we are targeting an RPC framework `yarpc` at version `1.0` we would have a tree like
```
instrumentation ->
...
yarpc-1.0 ->
auto
yarpc-1.0-auto.gradle
library
yarpc-1.0-library.gradle
testing
yarpc-1.0-testing.gradle
```
and in the top level `settings.gradle`
```groovy
include 'instrumentation:yarpc-1.0:agent'
include 'instrumentation:yarpc-1.0:library'
include 'instrumentation:yarpc-1.0:testing'
```
#### Writing manual instrumentation
Begin by writing the instrumentation for the library in `library`. This generally involves defining a
`Tracer` and using the typed tracers in our `instrumentation-common` library to create and annotate
spans as part of the implementation of an interceptor for the library. The module should generally
only depend on the OpenTelemetry API, `instrumentation-common`, and the instrumented library itself.
[instrumentation-library.gradle](./gradle/instrumentation-library.gradle) needs to be applied to
configure build tooling for the library, e.g., to prevent conflict between manual instrumentation
loaded by the user and by the agent, we make sure to create a shaded version with no dependencies
for use from the auto instrumentation at a separate package. To configure this, you must define
`ext.javaSubPackage` with the name of the sub package under `io.opentelemetry.auto` that the code
lives in. In this example, we would use `yarpc.v1_0`.
#### Writing unit tests
Once the instrumentation is completed, we add unit tests to the `testing` module. Tests will
generally apply to both manual and auto instrumentation, with the only difference being how a client
or server is initialized. In a manual test, there will be code calling into the instrumentation API
while in an auto test, it will generally just use the library's API as is. Create unit tests in an
abstract class with an abstract method that returns an instrumented object like a client. The class
should itself extend from `InstrumentationSpecification` to be recognized by Spock and include helper
methods for assertions.
After writing a test or two, go back to the `library` package, make sure it has a test dependency on the
`testing` submodule and add a test that inherits from the abstract test class. You should implement
the method to initialize the client using the library's mechanism to register interceptors, perhaps
a method like `registerInterceptor` or wrapping the result of a library factory when delegating. The
test should implement the `InstrumentationTestRunner` trait for common setup logic. If the tests
pass, manual instrumentation is working OK.
#### Writing auto instrumentation
Now that we have working instrumentation, we can implement auto instrumentation so users of the agent
do not have to modify their apps to use it. Make sure the `auto` submodule has a dependency on the
`library` submodule and a test dependency on the `testing` submodule. Auto instrumentation defines
classes to match against to generate bytecode for. You will often match against the class you used
in the unit test for manual instrumentation, for example the builder of a client. And then you could
match against the method that creates the builder, for example its constructor. Auto instrumentation
can inject byte code to be run after the constructor returns, which would invoke e.g.,
`registerInterceptor` and initialize the instrumentation. Often, the code inside the byte code
decorator will be identical to the one in the unit test you wrote above - the agent does the work for
initializing the instrumentation library, so a user doesn't have to.
With that written, let's add tests for the auto instrumentation. We basically want to ensure that
the instrumentation works without the user knowing about the instrumentation. Add a test that extends
the base class you wrote earlier, but in this, create a client using none of the APIs in our project,
only the ones offered by the library. Implement the `AgentTestRunner` trait for common setup logic,
add `@RunWith(SpockRunner.class)` for a bit more bytecode initialization needed for agent tests
and try running. All of the tests should pass for auto instrumentation too.
### Building
#### Snapshot builds

View File

@ -12,7 +12,7 @@ dependencies {
implementation group: 'org.slf4j', name: 'slf4j-simple', version: versions.slf4j
// ^ Generally a bad idea for libraries, but we're shadowing.
testImplementation project(':testing')
testImplementation project(':testing-common')
testImplementation group: 'org.mockito', name: 'mockito-core', version: '2.19.0'
testImplementation group: 'org.assertj', name: 'assertj-core', version: '1.7.1'
}

View File

@ -26,7 +26,7 @@ dependencies {
implementation deps.autoservice
implementation deps.slf4j
testImplementation project(':testing')
testImplementation project(':testing-common')
instrumentationMuzzle sourceSets.main.output
instrumentationMuzzle configurations.implementation

View File

@ -0,0 +1,35 @@
ext {
noShadowPublish = true
}
group = 'io.opentelemetry.instrumentation'
apply from: "$rootDir/gradle/java.gradle"
apply from: "$rootDir/gradle/publish.gradle"
apply plugin: 'com.github.johnrengelman.shadow'
archivesBaseName = projectDir.parentFile.name
dependencies {
// TODO(anuraaga): We currently include common instrumentation logic like decorators in the
// bootstrap, but we need to move it out so manual instrumentation does not depend on code from
// the agent, like Agent.
api project(':auto-bootstrap')
api deps.opentelemetryApi
testImplementation project(':testing-common')
}
shadowJar {
archiveClassifier = 'agent'
configurations = []
relocate "io.opentelemetry.instrumentation.${javaSubPackage}", "io.opentelemetry.auto.instrumentation.${javaSubPackage}.shaded"
}
afterEvaluate {
archivesBaseName = 'opentelemetry-' + archivesBaseName
}

View File

@ -3,6 +3,10 @@
apply plugin: 'net.bytebuddy.byte-buddy'
apply plugin: 'muzzle'
ext {
packageInAgentBundle = true
}
byteBuddy {
transformation {
// Applying NoOp optimizes build by applying bytebuddy plugin to only compileJava task
@ -21,6 +25,8 @@ tasks.withType(Test) {
}
afterEvaluate {
archivesBaseName = 'opentelemetry-auto-' + archivesBaseName
byteBuddy {
transformation {
tasks = ['compileJava', 'compileScala', 'compileKotlin']
@ -34,7 +40,7 @@ afterEvaluate {
// Apply common dependencies for instrumentation.
implementation(project(':auto-tooling')) {
// OpenTelemetry SDK is not needed for compilation, and :opentelemetry-sdk-shaded-for-testing
// is brought in for tests by project(:testing) below
// is brought in for tests by project(:testing-common) below
exclude group: 'io.opentelemetry', module: 'opentelemetry-sdk'
}
implementation deps.bytebuddy
@ -48,7 +54,7 @@ afterEvaluate {
//testImplementation project(':instrumentation:http-url-connection')
testImplementation project(':instrumentation:java-classloader')
testImplementation project(':testing')
testImplementation project(':testing-common')
testAnnotationProcessor deps.autoservice
testImplementation deps.autoservice
testImplementation project(':utils:test-utils')

View File

@ -1,63 +0,0 @@
plugins {
id "com.github.johnrengelman.shadow"
}
ext {
minJavaVersionForTests = JavaVersion.VERSION_1_8
noShadowPublish = true
}
group = 'io.opentelemetry.instrumentation'
apply from: "$rootDir/gradle/java.gradle"
apply from: "$rootDir/gradle/publish.gradle"
apply plugin: 'org.unbroken-dome.test-sets'
testSets {
latestDepTest
}
dependencies {
// TODO(anuraaga): We currently include common instrumentation logic like decorators in the
// bootstrap, but we need to move it out so manual instrumentation does not depend on code from
// the agent, like Agent.
api project(':auto-bootstrap')
api deps.opentelemetryApi
api group: 'software.amazon.awssdk', name: 'aws-core', version: '2.2.0'
testImplementation project(':testing')
// Include httpclient instrumentation for testing because it is a dependency for aws-sdk.
testImplementation project(':instrumentation:apache-httpclient:apache-httpclient-4.0')
// Also include netty instrumentation because it is used by aws async client
testImplementation project(':instrumentation:netty:netty-4.1')
testImplementation group: 'software.amazon.awssdk', name: 'apache-client', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 's3', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 'rds', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 'ec2', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 'sqs', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 'dynamodb', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 'kinesis', version: '2.2.0'
testImplementation deps.guava
latestDepTestImplementation project(':instrumentation:apache-httpclient:apache-httpclient-4.0')
latestDepTestImplementation project(':instrumentation:netty:netty-4.1')
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'apache-client', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 's3', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'rds', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'ec2', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'sqs', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'dynamodb', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'kinesis', version: '+'
}
shadowJar {
archiveClassifier = 'agent'
configurations = []
relocate 'io.opentelemetry.instrumentation.awssdk.v2_2', 'io.opentelemetry.auto.instrumentation.awssdk.v2_2.shaded'
}

View File

@ -41,7 +41,7 @@ dependencies {
implementation deps.opentelemetryApi
implementation deps.slf4j
testImplementation project(':testing')
testImplementation project(':testing-common')
latestDepTestImplementation group: 'io.projectreactor', name: 'reactor-core', version: '3.+'
// Looks like later versions on reactor need this dependency for some reason even though it is marked as optional.

View File

@ -0,0 +1,39 @@
ext {
minJavaVersionForTests = JavaVersion.VERSION_1_8
}
apply from: "$rootDir/gradle/instrumentation.gradle"
apply plugin: 'org.unbroken-dome.test-sets'
// TODO(anuraaga): Move into instrumentation.gradle
archivesBaseName = projectDir.parentFile.name
muzzle {
pass {
group = "software.amazon.awssdk"
module = "aws-core"
versions = "[2.2.0,)"
}
}
testSets {
latestDepTest {
dirName = 'test'
}
}
dependencies {
implementation project(path: ':instrumentation:aws-sdk:aws-sdk-2.2:library', configuration: 'shadow')
compileOnly group: 'software.amazon.awssdk', name: 'aws-core', version: '2.2.0'
testImplementation project(':instrumentation:aws-sdk:aws-sdk-2.2:testing')
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'apache-client', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 's3', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'rds', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'ec2', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'sqs', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'dynamodb', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'kinesis', version: '+'
}

View File

@ -0,0 +1,28 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import io.opentelemetry.auto.test.AgentTestTrait
import io.opentelemetry.auto.test.SpockRunner
import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2ClientTest
import org.junit.runner.RunWith
import software.amazon.awssdk.core.client.builder.SdkClientBuilder
@RunWith(SpockRunner)
class Aws2ClientTest extends AbstractAws2ClientTest implements AgentTestTrait {
@Override
void configureSdkClient(SdkClientBuilder builder) {
}
}

View File

@ -1,48 +0,0 @@
ext {
minJavaVersionForTests = JavaVersion.VERSION_1_8
}
apply from: "$rootDir/gradle/instrumentation.gradle"
apply plugin: 'org.unbroken-dome.test-sets'
muzzle {
pass {
group = "software.amazon.awssdk"
module = "aws-core"
versions = "[2.2.0,)"
}
}
testSets {
latestDepTest
}
dependencies {
implementation project(path: ':instrumentation-core:aws-sdk:aws-sdk-2.2', configuration: 'shadow')
compileOnly group: 'software.amazon.awssdk', name: 'aws-core', version: '2.2.0'
// Include httpclient instrumentation for testing because it is a dependency for aws-sdk.
testImplementation project(':instrumentation:apache-httpclient:apache-httpclient-4.0')
// Also include netty instrumentation because it is used by aws async client
testImplementation project(':instrumentation:netty:netty-4.1')
testImplementation group: 'software.amazon.awssdk', name: 'apache-client', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 's3', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 'rds', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 'ec2', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 'sqs', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 'dynamodb', version: '2.2.0'
testImplementation group: 'software.amazon.awssdk', name: 'kinesis', version: '2.2.0'
testImplementation deps.guava
latestDepTestImplementation project(':instrumentation:apache-httpclient:apache-httpclient-4.0')
latestDepTestImplementation project(':instrumentation:netty:netty-4.1')
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'apache-client', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 's3', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'rds', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'ec2', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'sqs', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'dynamodb', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'kinesis', version: '+'
}

View File

@ -0,0 +1,27 @@
ext {
minJavaVersionForTests = JavaVersion.VERSION_1_8
javaSubPackage = 'awssdk.v2_2'
}
apply from: "$rootDir/gradle/instrumentation-library.gradle"
apply plugin: 'org.unbroken-dome.test-sets'
testSets {
latestDepTest {
dirName = 'test'
}
}
dependencies {
compileOnly group: 'software.amazon.awssdk', name: 'aws-core', version: '2.2.0'
testImplementation project(':instrumentation:aws-sdk:aws-sdk-2.2:testing')
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'apache-client', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 's3', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'rds', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'ec2', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'sqs', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'dynamodb', version: '+'
latestDepTestImplementation group: 'software.amazon.awssdk', name: 'kinesis', version: '+'
}

View File

@ -0,0 +1,30 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.opentelemetry.instrumentation.awssdk.v2_2
import io.opentelemetry.auto.test.InstrumentationTestTrait
import software.amazon.awssdk.core.client.builder.SdkClientBuilder
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
class Aws2ClientTest extends AbstractAws2ClientTest implements InstrumentationTestTrait {
@Override
void configureSdkClient(SdkClientBuilder builder) {
builder.overrideConfiguration(ClientOverrideConfiguration.builder()
.addExecutionInterceptor(AwsSdk.newInterceptor())
.build())
}
}

View File

@ -1,349 +0,0 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import io.opentelemetry.auto.bootstrap.instrumentation.decorator.HttpClientDecorator
import io.opentelemetry.auto.test.AgentTestRunner
import io.opentelemetry.trace.attributes.SemanticAttributes
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.core.ResponseInputStream
import software.amazon.awssdk.core.async.AsyncResponseTransformer
import software.amazon.awssdk.core.exception.SdkClientException
import software.amazon.awssdk.http.apache.ApacheHttpClient
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest
import software.amazon.awssdk.services.ec2.Ec2AsyncClient
import software.amazon.awssdk.services.ec2.Ec2Client
import software.amazon.awssdk.services.kinesis.KinesisClient
import software.amazon.awssdk.services.kinesis.model.DeleteStreamRequest
import software.amazon.awssdk.services.rds.RdsAsyncClient
import software.amazon.awssdk.services.rds.RdsClient
import software.amazon.awssdk.services.rds.model.DeleteOptionGroupRequest
import software.amazon.awssdk.services.s3.S3AsyncClient
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.CreateBucketRequest
import software.amazon.awssdk.services.s3.model.GetObjectRequest
import software.amazon.awssdk.services.sqs.SqsAsyncClient
import software.amazon.awssdk.services.sqs.SqsClient
import software.amazon.awssdk.services.sqs.model.CreateQueueRequest
import software.amazon.awssdk.services.sqs.model.SendMessageRequest
import spock.lang.AutoCleanup
import spock.lang.Shared
import java.time.Duration
import java.util.concurrent.Future
import java.util.concurrent.atomic.AtomicReference
import static io.opentelemetry.auto.test.server.http.TestHttpServer.httpServer
import static io.opentelemetry.trace.Span.Kind.CLIENT
class Aws2ClientTest extends AgentTestRunner {
private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = StaticCredentialsProvider
.create(AwsBasicCredentials.create("my-access-key", "my-secret-key"))
@Shared
def responseBody = new AtomicReference<String>()
@AutoCleanup
@Shared
def server = httpServer {
handlers {
all {
response.status(200).send(responseBody.get())
}
}
}
def "send #operation request with builder {#builder.class.getName()} mocked response"() {
setup:
def client = builder
.endpointOverride(server.address)
.region(Region.AP_NORTHEAST_1)
.credentialsProvider(CREDENTIALS_PROVIDER)
.build()
responseBody.set(body)
def response = call.call(client)
if (response instanceof Future) {
response = response.get()
}
expect:
response != null
response.class.simpleName.startsWith(operation) || response instanceof ResponseInputStream
assertTraces(1) {
trace(0, 1) {
span(0) {
operationName "$service.$operation"
spanKind CLIENT
errored false
parent()
attributes {
"${SemanticAttributes.NET_PEER_NAME.key()}" "localhost"
"${SemanticAttributes.NET_PEER_PORT.key()}" server.address.port
"${SemanticAttributes.HTTP_URL.key()}" { it.startsWith("${server.address}${path}") }
"${SemanticAttributes.HTTP_METHOD.key()}" "$method"
"${SemanticAttributes.HTTP_STATUS_CODE.key()}" 200
"${SemanticAttributes.HTTP_USER_AGENT.key()}" { it.startsWith("aws-sdk-java/") }
"aws.service" "$service"
"aws.operation" "${operation}"
"aws.agent" "java-aws-sdk"
"aws.requestId" "$requestId"
if (service == "S3") {
"aws.bucket.name" "somebucket"
} else if (service == "Sqs" && operation == "CreateQueue") {
"aws.queue.name" "somequeue"
} else if (service == "Sqs" && operation == "SendMessage") {
"aws.queue.url" "someurl"
} else if (service == "DynamoDb") {
"aws.table.name" "sometable"
} else if (service == "Kinesis") {
"aws.stream.name" "somestream"
}
}
}
}
}
server.lastRequest.headers.get("traceparent") == null
where:
service | operation | method | path | requestId | builder | call | body
"S3" | "CreateBucket" | "PUT" | "/somebucket" | "UNKNOWN" | S3Client.builder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | ""
"S3" | "GetObject" | "GET" | "/somebucket/somekey" | "UNKNOWN" | S3Client.builder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build()) } | ""
"DynamoDb" | "CreateTable" | "POST" | "/" | "UNKNOWN" | DynamoDbClient.builder() | { c -> c.createTable(CreateTableRequest.builder().tableName("sometable").build()) } | ""
"Kinesis" | "DeleteStream" | "POST" | "/" | "UNKNOWN" | KinesisClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | ""
"Sqs" | "CreateQueue" | "POST" | "/" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | """
<CreateQueueResponse>
<CreateQueueResult><QueueUrl>https://queue.amazonaws.com/123456789012/MyQueue</QueueUrl></CreateQueueResult>
<ResponseMetadata><RequestId>7a62c49f-347e-4fc4-9331-6e8e7a96aa73</RequestId></ResponseMetadata>
</CreateQueueResponse>
"""
"Sqs" | "SendMessage" | "POST" | "/" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl("someurl").messageBody("").build()) } | """
<SendMessageResponse>
<SendMessageResult>
<MD5OfMessageBody>d41d8cd98f00b204e9800998ecf8427e</MD5OfMessageBody>
<MD5OfMessageAttributes>3ae8f24a165a8cedc005670c81a27295</MD5OfMessageAttributes>
<MessageId>5fea7756-0ea4-451a-a703-a558b933e274</MessageId>
</SendMessageResult>
<ResponseMetadata><RequestId>27daac76-34dd-47df-bd01-1f6e873584a0</RequestId></ResponseMetadata>
</SendMessageResponse>
"""
"Ec2" | "AllocateAddress" | "POST" | "/" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | Ec2Client.builder() | { c -> c.allocateAddress() } | """
<AllocateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<publicIp>192.0.2.1</publicIp>
<domain>standard</domain>
</AllocateAddressResponse>
"""
"Rds" | "DeleteOptionGroup" | "POST" | "/" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | RdsClient.builder() | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """
<DeleteOptionGroupResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
<ResponseMetadata><RequestId>0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99</RequestId></ResponseMetadata>
</DeleteOptionGroupResponse>
"""
}
def "send #operation async request with builder {#builder.class.getName()} mocked response"() {
setup:
def client = builder
.endpointOverride(server.address)
.region(Region.AP_NORTHEAST_1)
.credentialsProvider(CREDENTIALS_PROVIDER)
.build()
responseBody.set(body)
def response = call.call(client)
if (response instanceof Future) {
response = response.get()
}
expect:
response != null
assertTraces(2) {
trace(0, 1) {
span(0) {
operationName "$service.$operation"
spanKind CLIENT
errored false
parent()
attributes {
"${SemanticAttributes.NET_PEER_NAME.key()}" "localhost"
"${SemanticAttributes.NET_PEER_PORT.key()}" server.address.port
"${SemanticAttributes.HTTP_URL.key()}" { it.startsWith("${server.address}${path}") }
"${SemanticAttributes.HTTP_METHOD.key()}" "$method"
"${SemanticAttributes.HTTP_STATUS_CODE.key()}" 200
"${SemanticAttributes.HTTP_USER_AGENT.key()}" { it.startsWith("aws-sdk-java/") }
"aws.service" "$service"
"aws.operation" "${operation}"
"aws.agent" "java-aws-sdk"
"aws.requestId" "$requestId"
if (service == "S3") {
"aws.bucket.name" "somebucket"
} else if (service == "Sqs" && operation == "CreateQueue") {
"aws.queue.name" "somequeue"
} else if (service == "Sqs" && operation == "SendMessage") {
"aws.queue.url" "someurl"
} else if (service == "DynamoDb") {
"aws.table.name" "sometable"
} else if (service == "Kinesis") {
"aws.stream.name" "somestream"
}
}
}
}
// TODO: this should be part of the same trace but netty instrumentation doesn't cooperate
trace(1, 1) {
span(0) {
operationName expectedOperationName(method)
spanKind CLIENT
errored false
parent()
attributes {
"${SemanticAttributes.NET_PEER_NAME.key()}" "localhost"
"${SemanticAttributes.NET_PEER_IP.key()}" "127.0.0.1"
"${SemanticAttributes.NET_PEER_PORT.key()}" server.address.port
"${SemanticAttributes.HTTP_URL.key()}" { it.startsWith("${server.address}${path}") }
"${SemanticAttributes.HTTP_METHOD.key()}" "$method"
"${SemanticAttributes.HTTP_STATUS_CODE.key()}" 200
"${SemanticAttributes.HTTP_USER_AGENT.key()}" { it.startsWith("aws-sdk-java/") }
}
}
}
}
server.lastRequest.headers.get("traceparent") == null
where:
service | operation | method | path | requestId | builder | call | body
"S3" | "CreateBucket" | "PUT" | "/somebucket" | "UNKNOWN" | S3AsyncClient.builder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | ""
"S3" | "GetObject" | "GET" | "/somebucket/somekey" | "UNKNOWN" | S3AsyncClient.builder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build(), AsyncResponseTransformer.toBytes()) } | "1234567890"
"DynamoDb" | "CreateTable" | "POST" | "/" | "UNKNOWN" | DynamoDbAsyncClient.builder() | { c -> c.createTable(CreateTableRequest.builder().tableName("sometable").build()) } | ""
// Kinesis seems to expect an http2 response which is incompatible with our test server.
// "Kinesis" | "DeleteStream" | "POST" | "/" | "UNKNOWN" | KinesisAsyncClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | ""
"Sqs" | "CreateQueue" | "POST" | "/" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsAsyncClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | """
<CreateQueueResponse>
<CreateQueueResult><QueueUrl>https://queue.amazonaws.com/123456789012/MyQueue</QueueUrl></CreateQueueResult>
<ResponseMetadata><RequestId>7a62c49f-347e-4fc4-9331-6e8e7a96aa73</RequestId></ResponseMetadata>
</CreateQueueResponse>
"""
"Sqs" | "SendMessage" | "POST" | "/" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsAsyncClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl("someurl").messageBody("").build()) } | """
<SendMessageResponse>
<SendMessageResult>
<MD5OfMessageBody>d41d8cd98f00b204e9800998ecf8427e</MD5OfMessageBody>
<MD5OfMessageAttributes>3ae8f24a165a8cedc005670c81a27295</MD5OfMessageAttributes>
<MessageId>5fea7756-0ea4-451a-a703-a558b933e274</MessageId>
</SendMessageResult>
<ResponseMetadata><RequestId>27daac76-34dd-47df-bd01-1f6e873584a0</RequestId></ResponseMetadata>
</SendMessageResponse>
"""
"Ec2" | "AllocateAddress" | "POST" | "/" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | Ec2AsyncClient.builder() | { c -> c.allocateAddress() } | """
<AllocateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<publicIp>192.0.2.1</publicIp>
<domain>standard</domain>
</AllocateAddressResponse>
"""
"Rds" | "DeleteOptionGroup" | "POST" | "/" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | RdsAsyncClient.builder() | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """
<DeleteOptionGroupResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
<ResponseMetadata><RequestId>0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99</RequestId></ResponseMetadata>
</DeleteOptionGroupResponse>
"""
}
def "client can be customized by user"() {
setup:
def client = DynamoDbClient.builder()
.endpointOverride(server.address)
.region(Region.AP_NORTHEAST_1)
.credentialsProvider(CREDENTIALS_PROVIDER)
.overrideConfiguration {
it.putHeader("x-name", "value")
}
.build()
when:
responseBody.set("")
client.createTable(CreateTableRequest.builder().tableName("sometable").build())
then:
assertTraces(1) {
trace(0, 1) {
span(0) {}
}
}
server.lastRequest.headers.get("x-name") == "value"
cleanup:
server.close()
}
// TODO(anuraaga): Add events for retries.
def "timeout and retry errors not captured"() {
setup:
def server = httpServer {
handlers {
all {
Thread.sleep(500)
response.status(200).send()
}
}
}
def client = S3Client.builder()
.endpointOverride(server.address)
.region(Region.AP_NORTHEAST_1)
.credentialsProvider(CREDENTIALS_PROVIDER)
.httpClientBuilder(ApacheHttpClient.builder().socketTimeout(Duration.ofMillis(50)))
.build()
when:
client.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build())
then:
thrown SdkClientException
assertTraces(1) {
trace(0, 1) {
span(0) {
operationName "S3.GetObject"
spanKind CLIENT
errored true
parent()
attributes {
"${SemanticAttributes.NET_PEER_NAME.key()}" "localhost"
"${SemanticAttributes.NET_PEER_PORT.key()}" server.address.port
"${SemanticAttributes.HTTP_URL.key()}" "$server.address/somebucket/somekey"
"${SemanticAttributes.HTTP_METHOD.key()}" "GET"
"aws.service" "S3"
"aws.operation" "GetObject"
"aws.agent" "java-aws-sdk"
"aws.bucket.name" "somebucket"
errorAttributes SdkClientException, "Unable to execute HTTP request: Read timed out"
}
}
}
}
cleanup:
server.close()
}
String expectedOperationName(String method) {
return method != null ? "HTTP $method" : HttpClientDecorator.DEFAULT_SPAN_NAME
}
}

View File

@ -0,0 +1,19 @@
apply from: "$rootDir/gradle/java.gradle"
dependencies {
api project(':testing-common')
api group: 'software.amazon.awssdk', name: 'apache-client', version: '2.2.0'
api group: 'software.amazon.awssdk', name: 's3', version: '2.2.0'
api group: 'software.amazon.awssdk', name: 'rds', version: '2.2.0'
api group: 'software.amazon.awssdk', name: 'ec2', version: '2.2.0'
api group: 'software.amazon.awssdk', name: 'sqs', version: '2.2.0'
api group: 'software.amazon.awssdk', name: 'dynamodb', version: '2.2.0'
api group: 'software.amazon.awssdk', name: 'kinesis', version: '2.2.0'
implementation deps.guava
implementation deps.groovy
implementation deps.opentelemetryApi
implementation deps.spock
}

View File

@ -14,15 +14,15 @@
* limitations under the License.
*/
import io.opentelemetry.auto.bootstrap.instrumentation.decorator.HttpClientDecorator
import io.opentelemetry.auto.test.InstrumentationTestRunner
import io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdk
package io.opentelemetry.instrumentation.awssdk.v2_2
import io.opentelemetry.auto.test.InstrumentationSpecification
import io.opentelemetry.trace.attributes.SemanticAttributes
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.core.ResponseInputStream
import software.amazon.awssdk.core.async.AsyncResponseTransformer
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
import software.amazon.awssdk.core.client.builder.SdkClientBuilder
import software.amazon.awssdk.core.exception.SdkClientException
import software.amazon.awssdk.http.apache.ApacheHttpClient
import software.amazon.awssdk.regions.Region
@ -54,7 +54,7 @@ import java.util.concurrent.atomic.AtomicReference
import static io.opentelemetry.auto.test.server.http.TestHttpServer.httpServer
import static io.opentelemetry.trace.Span.Kind.CLIENT
class Aws2ClientCoreTest extends InstrumentationTestRunner {
abstract class AbstractAws2ClientTest extends InstrumentationSpecification {
private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = StaticCredentialsProvider
.create(AwsBasicCredentials.create("my-access-key", "my-secret-key"))
@ -72,15 +72,15 @@ class Aws2ClientCoreTest extends InstrumentationTestRunner {
}
}
def "send #operation request with builder {#builder.class.getName()} mocked response"() {
abstract void configureSdkClient(SdkClientBuilder builder)
def "send #operation request with builder #builder.class.getName() mocked response"() {
setup:
configureSdkClient(builder)
def client = builder
.endpointOverride(server.address)
.region(Region.AP_NORTHEAST_1)
.credentialsProvider(CREDENTIALS_PROVIDER)
.overrideConfiguration(ClientOverrideConfiguration.builder()
.addExecutionInterceptor(AwsSdk.newInterceptor())
.build())
.build()
responseBody.set(body)
def response = call.call(client)
@ -164,15 +164,13 @@ class Aws2ClientCoreTest extends InstrumentationTestRunner {
"""
}
def "send #operation async request with builder {#builder.class.getName()} mocked response"() {
def "send #operation async request with builder #builder.class.getName() mocked response"() {
setup:
configureSdkClient(builder)
def client = builder
.endpointOverride(server.address)
.region(Region.AP_NORTHEAST_1)
.credentialsProvider(CREDENTIALS_PROVIDER)
.overrideConfiguration(ClientOverrideConfiguration.builder()
.addExecutionInterceptor(AwsSdk.newInterceptor())
.build())
.build()
responseBody.set(body)
def response = call.call(client)
@ -269,13 +267,12 @@ class Aws2ClientCoreTest extends InstrumentationTestRunner {
}
}
}
def client = S3Client.builder()
def builder = S3Client.builder()
configureSdkClient(builder)
def client = builder
.endpointOverride(server.address)
.region(Region.AP_NORTHEAST_1)
.credentialsProvider(CREDENTIALS_PROVIDER)
.overrideConfiguration(ClientOverrideConfiguration.builder()
.addExecutionInterceptor(AwsSdk.newInterceptor())
.build())
.httpClientBuilder(ApacheHttpClient.builder().socketTimeout(Duration.ofMillis(50)))
.build()
@ -314,4 +311,5 @@ class Aws2ClientCoreTest extends InstrumentationTestRunner {
String expectedOperationName(String method) {
return method != null ? "HTTP $method" : HttpClientDecorator.DEFAULT_SPAN_NAME
}
}

View File

@ -21,13 +21,13 @@ Project instr_project = project
subprojects {
afterEvaluate { Project subProj ->
if (subProj.getPlugins().hasPlugin('java')) {
subProj.archivesBaseName = 'opentelemetry-auto-' + subProj.archivesBaseName
// Make it so all instrumentation subproject tests can be run with a single command.
instr_project.tasks.test.dependsOn(subProj.tasks.test)
instr_project.dependencies {
implementation(project(subProj.getPath()))
if (subProj.findProperty('packageInAgentBundle')) {
instr_project.dependencies {
implementation(project(subProj.getPath()))
}
}
}
}

View File

@ -21,7 +21,7 @@ dependencies {
implementation project(':instrumentation:servlet:servlet-3.0')
// Don't want to conflict with jetty from the test server.
testImplementation(project(':testing')) {
testImplementation(project(':testing-common')) {
exclude group: 'org.eclipse.jetty', module: 'jetty-server'
}

View File

@ -17,7 +17,7 @@ testSets {
configurations {
// In order to test the real log4j library we need to remove the log4j transitive
// dependency 'log4j-over-slf4j' brought in by :testing which would shadow
// dependency 'log4j-over-slf4j' brought in by :testing-common which would shadow
// the log4j module under test using a proxy to slf4j instead.
testImplementation.exclude group: 'org.slf4j', module: 'log4j-over-slf4j'

View File

@ -23,7 +23,7 @@ testSets {
configurations {
// In order to test the real log4j library we need to remove the log4j transitive
// dependency 'log4j-over-slf4j' brought in by :testing which would shadow
// dependency 'log4j-over-slf4j' brought in by :testing-common which would shadow
// the log4j module under test using a proxy to slf4j instead.
testImplementation.exclude group: 'org.slf4j', module: 'log4j-over-slf4j'
}

View File

@ -25,7 +25,6 @@ dependencies {
implementation project(':auto-tooling')
testImplementation project(':testing')
testImplementation project(':instrumentation:java-concurrent')
testImplementation group: 'com.squareup.okhttp', name: 'okhttp', version: '2.2.0'

View File

@ -30,7 +30,7 @@ dependencies {
implementation project(':auto-tooling')
testImplementation(project(':testing')) {
testImplementation(project(':testing-common')) {
exclude module: 'okhttp'
}
testImplementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.0.0'

View File

@ -26,7 +26,7 @@ dependencies {
compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.2'
api(project(':instrumentation-core:servlet'))
testImplementation(project(':testing')) {
testImplementation(project(':testing-common')) {
exclude group: 'org.eclipse.jetty', module: 'jetty-server'
}
testImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '7.0.0.v20091005'

View File

@ -25,7 +25,7 @@ dependencies {
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '3.0.1'
api(project(':instrumentation-core:servlet'))
testImplementation(project(':testing')) {
testImplementation(project(':testing-common')) {
exclude group: 'org.eclipse.jetty', module: 'jetty-server'
}
testImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '8.0.0.v20110901'

View File

@ -17,7 +17,7 @@ testSets {
dependencies {
compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.3'
testImplementation(project(':testing')) {
testImplementation(project(':testing-common')) {
exclude group: 'org.eclipse.jetty', module: 'jetty-server'
}
testImplementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '7.0.0.v20091005'

View File

@ -27,7 +27,7 @@ dependencies {
// compileOnly group: 'org.springframework', name: 'spring-webmvc', version: '2.5.6'
// compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.4'
testImplementation(project(':testing')) {
testImplementation(project(':testing-common')) {
exclude(module: 'jetty-server') // incompatable servlet api
}

View File

@ -28,7 +28,7 @@ include ':agent-tooling'
include ':load-generator'
// misc
include ':testing'
include ':testing-common'
include ':utils:test-utils'
include ':utils:thread-utils'
@ -47,7 +47,9 @@ include ':instrumentation:apache-httpasyncclient-4.0'
include ':instrumentation:apache-httpclient:apache-httpclient-2.0'
include ':instrumentation:apache-httpclient:apache-httpclient-4.0'
include ':instrumentation:aws-sdk:aws-sdk-1.11'
include ':instrumentation:aws-sdk:aws-sdk-2.2'
include ':instrumentation:aws-sdk:aws-sdk-2.2:auto'
include ':instrumentation:aws-sdk:aws-sdk-2.2:library'
include ':instrumentation:aws-sdk:aws-sdk-2.2:testing'
include ':instrumentation:cassandra:cassandra-3.0'
include ':instrumentation:cassandra:cassandra-4.0'
include ':instrumentation:cdi-testing'
@ -151,7 +153,6 @@ include ':instrumentation:vertx-3.0'
include ':instrumentation:vertx-reactive-3.5'
include ':instrumentation-core:apache-httpclient-4.0'
include ':instrumentation-core:aws-sdk:aws-sdk-2.2'
include ':instrumentation-core:reactor-3.1'
include ':instrumentation-core:servlet'
include ':instrumentation-core:spring:spring-boot-autoconfigure'
@ -173,7 +174,11 @@ include ':benchmark-integration:jetty-perftest'
include ':benchmark-integration:play-perftest'
def setBuildFile(project) {
project.buildFileName = "${project.name}.gradle"
if (['auto', 'library', 'testing'].contains(project.projectDir.name)) {
project.buildFileName = "${project.projectDir.parentFile.name}-${project.projectDir.name}.gradle"
} else {
project.buildFileName = "${project.name}.gradle"
}
project.children.each {
setBuildFile(it)
}

View File

@ -4,7 +4,7 @@ description = 'smoke-tests'
dependencies {
api deps.spock
api project(':testing')
api project(':testing-common')
implementation project(':auto-exporters:opentelemetry-auto-exporter-logging')
implementation deps.slf4j

View File

@ -171,7 +171,6 @@ public abstract class AgentTestRunner extends AgentSpecification {
public void beforeTest() {
assert !getTestTracer().getCurrentSpan().getContext().isValid()
: "Span is active before test has started: " + getTestTracer().getCurrentSpan();
log.debug("Starting test: '{}'", getSpecificationContext().getCurrentIteration().getName());
TEST_WRITER.clear();
}

View File

@ -0,0 +1,94 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.opentelemetry.auto.test
import com.google.common.base.Predicate
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import io.opentelemetry.auto.test.asserts.InMemoryExporterAssert
import io.opentelemetry.sdk.trace.data.SpanData
/**
* A trait which initializes agent tests, including bytecode manipulation and a test span exporter.
* All agent tests should implement this trait.
*/
trait AgentTestTrait {
static AgentTestRunner agentTestRunner
static InMemoryExporter testWriter
def setupSpec() {
agentTestRunner = new AgentTestRunnerImpl()
testWriter = AgentTestRunner.TEST_WRITER
AgentTestRunner.agentSetup()
agentTestRunner.setupBeforeTests()
childSetupSpec()
}
def setup() {
agentTestRunner.beforeTest()
childSetup()
}
def cleanupSpec() {
agentTestRunner.cleanUpAfterTests()
AgentTestRunner.agentCleanup()
}
/**
* Initialization method called once per individual test. Equivalent to Spock's {@code setup} which
* we can't use because of https://stackoverflow.com/questions/56464191/public-groovy-method-must-be-public-says-the-compiler
*/
def childSetup() {}
/**
* Initialization method called once per test class. Equivalent to Spock's {@code setupSpec} which
* we can't use because of https://stackoverflow.com/questions/56464191/public-groovy-method-must-be-public-says-the-compiler
*/
def childSetupSpec() {}
/**
* Cleanup method called once per test class. Equivalent to Spock's {@code cleanupSpec} which
* we can't use because of https://stackoverflow.com/questions/56464191/public-groovy-method-must-be-public-says-the-compiler
*/
def childCleanupSpec() {}
void assertTraces(final int size,
@ClosureParams(
value = SimpleType,
options = "io.opentelemetry.auto.test.asserts.ListWriterAssert")
@DelegatesTo(value = InMemoryExporterAssert, strategy = Closure.DELEGATE_FIRST)
final Closure spec) {
AgentTestRunner.assertTraces(size, spec)
}
void assertTracesWithFilter(
final int size,
final Predicate<List<SpanData>> excludes,
@ClosureParams(
value = SimpleType,
options = "io.opentelemetry.auto.test.asserts.ListWriterAssert")
@DelegatesTo(value = InMemoryExporterAssert, strategy = Closure.DELEGATE_FIRST)
final Closure spec) {
AgentTestRunner.assertTracesWithFilter(size, excludes, spec)
}
static class AgentTestRunnerImpl extends AgentTestRunner {}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.opentelemetry.auto.test
import com.google.common.base.Predicate
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import io.opentelemetry.auto.test.asserts.InMemoryExporterAssert
import io.opentelemetry.sdk.trace.data.SpanData
import spock.lang.Specification
/**
* Base class for test specifications that are shared between instrumentation libraries and agent.
* The methods in this class are implemented by {@link AgentTestTrait} and
* {@link InstrumentationTestTrait}.
*/
abstract class InstrumentationSpecification extends Specification {
abstract void assertTraces(
final int size,
@ClosureParams(
value = SimpleType,
options = "io.opentelemetry.auto.test.asserts.ListWriterAssert")
@DelegatesTo(value = InMemoryExporterAssert, strategy = Closure.DELEGATE_FIRST)
final Closure spec)
abstract void assertTracesWithFilter(
final int size,
final Predicate<List<SpanData>> excludes,
@ClosureParams(
value = SimpleType,
options = "io.opentelemetry.auto.test.asserts.ListWriterAssert")
@DelegatesTo(value = InMemoryExporterAssert, strategy = Closure.DELEGATE_FIRST)
final Closure spec)
}

View File

@ -0,0 +1,76 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.opentelemetry.auto.test
import com.google.common.base.Predicate
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import io.opentelemetry.auto.test.asserts.InMemoryExporterAssert
import io.opentelemetry.sdk.trace.data.SpanData
/**
* A trait which initializes instrumentation library tests, including a test span exporter. All
* library tests should implement this trait.
*/
trait InstrumentationTestTrait {
static InstrumentationTestRunner instrumentationTestRunner
static InMemoryExporter testWriter
def setupSpec() {
instrumentationTestRunner = new InstrumentationTestRunnerImpl()
testWriter = InstrumentationTestRunner.TEST_WRITER
}
def setup() {
instrumentationTestRunner.beforeTest()
}
/**
* Initialization method called once per test class. Equivalent to Spock's {@code setupSpec} which
* we can't use because of https://stackoverflow.com/questions/56464191/public-groovy-method-must-be-public-says-the-compiler
*/
def childSetupSpec() {}
/**
* Initialization method called once per individual test. Equivalent to Spock's {@code setup} which
* we can't use because of https://stackoverflow.com/questions/56464191/public-groovy-method-must-be-public-says-the-compiler
*/
def childSetup() {}
void assertTraces(final int size,
@ClosureParams(
value = SimpleType,
options = "io.opentelemetry.auto.test.asserts.ListWriterAssert")
@DelegatesTo(value = InMemoryExporterAssert, strategy = Closure.DELEGATE_FIRST)
final Closure spec) {
instrumentationTestRunner.assertTraces(size, spec)
}
void assertTracesWithFilter(
final int size,
final Predicate<List<SpanData>> excludes,
@ClosureParams(
value = SimpleType,
options = "io.opentelemetry.auto.test.asserts.ListWriterAssert")
@DelegatesTo(value = InMemoryExporterAssert, strategy = Closure.DELEGATE_FIRST)
final Closure spec) {
instrumentationTestRunner.assertTracesWithFilter(size, spec)
}
static class InstrumentationTestRunnerImpl extends InstrumentationTestRunner {}
}