diff options
Diffstat (limited to 'contrib/agent')
34 files changed, 2634 insertions, 0 deletions
diff --git a/contrib/agent/README.md b/contrib/agent/README.md new file mode 100644 index 00000000..f24c28a2 --- /dev/null +++ b/contrib/agent/README.md @@ -0,0 +1,95 @@ +# OpenCensus Agent for Java + +[![Build Status][travis-image]][travis-url] +[![Windows Build Status][appveyor-image]][appveyor-url] +[![Maven Central][maven-image]][maven-url] + +The *OpenCensus Agent for Java* collects and sends latency data about your Java process to +OpenCensus backends such as Zipkin, Stackdriver Trace, etc. for analysis and visualization. + + +## Features + +The *OpenCensus Agent for Java* is in an early development stage. The following features are +currently implemented: + +TODO(stschmidt): Update README.md along with implementation. + + +### Automatic context propagation for Executors + +The context of the caller of [Executor#execute](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html#execute-java.lang.Runnable-) +is automatically propagated to the submitted Runnable. + + +### Automatic context propagation for Threads + +The context of the caller of [Thread#start](https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#start--) +is automatically propagated to the new thread. + + +### Preliminary support for tracing + +As a proof-of-concept, the agent wraps the execution of +[URL#getContent](https://docs.oracle.com/javase/8/docs/api/java/net/URL.html#getContent--) in a new +trace span. + + +## Design Ideas + +We see tracing as a cross-cutting concern which the *OpenCensus Agent for Java* weaves into +existing Java bytecode (the application and its libraries) at runtime, typically when first loading +the concerned bytecode. + +This approach allows us to instrument arbitrary code without having to touch the source code of the +application or its dependencies. Furthermore, we don't require the application owner to upgrade any +of the application's third-party dependencies to specific versions. As long as the interface (e.g. +[java.sql.Driver#connect](https://docs.oracle.com/javase/8/docs/api/java/sql/Driver.html#connect-java.lang.String-java.util.Properties-)) +stays as-is across the supported versions, the Java agent's bytecode weaver will be able to +instrument the code. + +The *OpenCensus Agent for Java* uses [Byte Buddy](http://bytebuddy.net/), a widely used and +well-maintained bytecode manipulation library, for instrumenting selected Java methods at class +load-time. Which Java methods we want to intercept/instrument obviously depends on the library +(MongoDB vs. Redis, etc.) and the application. + + +## Installation and Usage + +Download the latest version of the *OpenCensus Agent for Java* `.jar` file +from [Maven Central][maven-url]. Store it somewhere on disk. + +To enable the *OpenCensus Agent for Java* for your application, add the option +`-javaagent:path/to/opencensus-contrib-agent-X.Y.Z.jar` to the invocation of the `java` +executable as shown in the following example. Replace `X.Y.Z` with the actual version number. + +```shell +java -javaagent:path/to/opencensus-contrib-agent-X.Y.Z.jar ... +``` + + +## Configuration + +The *OpenCensus Agent for Java* uses [Typesafe's configuration +library](https://lightbend.github.io/config/) for all user-configurable settings. Please refer to +[reference.conf](src/main/resources/reference.conf) for the available configuration knobs and their +defaults. + +You can override the default configuration in [different +ways](https://github.com/lightbend/config/blob/7cae92d3ae3ff9d06f1db43800232d2f73c6fe44/README.md#standard-behavior). +For example, to disable the automatic context propagation for Executors, add a system property as +follows: + +```shell +java -javaagent:path/to/opencensus-contrib-agent-X.Y.Z.jar \ + -Dopencensus.contrib.agent.context-propagation.executor.enabled=false \ + ... +``` + + +[travis-image]: https://travis-ci.org/census-instrumentation/opencensus-java.svg?branch=master +[travis-url]: https://travis-ci.org/census-instrumentation/opencensus-java +[appveyor-image]: https://ci.appveyor.com/api/projects/status/hxthmpkxar4jq4be/branch/master?svg=true +[appveyor-url]: https://ci.appveyor.com/project/opencensusjavateam/opencensus-java/branch/master +[maven-image]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-agent/badge.svg +[maven-url]: https://maven-badges.herokuapp.com/maven-central/io.opencensus/opencensus-contrib-agent diff --git a/contrib/agent/build.gradle b/contrib/agent/build.gradle new file mode 100644 index 00000000..11271a42 --- /dev/null +++ b/contrib/agent/build.gradle @@ -0,0 +1,246 @@ +plugins { + id 'com.github.johnrengelman.shadow' version '2.0.2' +} + +description = 'OpenCensus Agent' + +def agentPackage = 'io.opencensus.contrib.agent' +def agentMainClass = "${agentPackage}.AgentMain" + +// The package containing the classes that need to be loaded by the bootstrap classloader because +// they are used from classes loaded by the bootstrap classloader. +def agentBootstrapPackage = "${agentPackage}.bootstrap" +def agentBootstrapPackageDir = agentBootstrapPackage.replace('.', '/') + '/' +def agentBootstrapClasses = agentBootstrapPackageDir + '**' + +// The package to which we relocate all third party packages. This avoids any conflicts of the +// agent's classes with the app's classes, which are loaded by the same classloader (the system +// classloader). +def agentRepackaged = "${agentPackage}.deps" + +dependencies { + compileOnly libraries.auto_service + compileOnly libraries.grpc_context + compileOnly project(':opencensus-api') + compile libraries.byte_buddy + compile libraries.config + compile libraries.findbugs_annotations + compile libraries.guava + + signature 'org.codehaus.mojo.signature:java17:1.0@signature' +} + +jar { + manifest { + // Set the required manifest attributes for the Java agent, cf. + // https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html. + attributes 'Premain-Class': agentMainClass + attributes 'Can-Retransform-Classes': true + } +} + +// Create bootstrap.jar containing the classes that need to be loaded by the bootstrap +// classloader. +task bootstrapJar(type: Jar) { + // Output to 'bootstrap.jar'. + baseName = 'bootstrap' + version = null + + from sourceSets.main.output + include agentBootstrapClasses +} + +shadowJar.dependsOn bootstrapJar + +// Bundle the agent's classes and dependencies into a single, self-contained JAR file. +shadowJar { + // Output to opencensus-contrib-agent-VERSION.jar. + classifier = null + + // Include only the following dependencies (excluding transitive dependencies). + dependencies { + include(dependency(libraries.byte_buddy)) + include(dependency(libraries.config)) + include(dependency(libraries.guava)) + } + + // Exclude cruft which still snuck in. + exclude 'META-INF/maven/**' + exclude agentBootstrapClasses + + // Relocate third party packages to avoid any conflicts of the agent's classes with the app's + // classes, which are loaded by the same classloader (the system classloader). + // Byte Buddy: + relocate 'net.bytebuddy', agentRepackaged + '.bytebuddy' + // Config: + relocate 'com.typesafe.config', agentRepackaged + '.config' + // Guava: + relocate 'com.google.common', agentRepackaged + '.guava' + relocate 'com.google.thirdparty.publicsuffix', agentRepackaged + '.publicsuffix' + + doLast { + def agentPackageDir = agentPackage.replace('.', '/') + '/' + def agentBootstrapJar = agentPackageDir + 'bootstrap.jar' + + // Bundle bootstrap.jar. + ant.jar(update: 'true', destfile: shadowJar.archivePath) { + mappedresources { + fileset(file: bootstrapJar.archivePath) + globmapper(from: '*', to: agentBootstrapJar) + } + } + + // Assert that there's nothing obviously wrong with the JAR's contents. + new java.util.zip.ZipFile(shadowJar.archivePath).withCloseable { + // Must have bundled the bootstrap.jar. + assert it.entries().any { it.name == agentBootstrapJar } + + it.entries().each { entry -> + // Must not contain anything outside of ${agentPackage}, ... + assert entry.name.startsWith(agentPackageDir) || + // ... except for the expected entries. + [ agentPackageDir, + 'META-INF/MANIFEST.MF', + 'META-INF/services/io.opencensus.contrib.agent.instrumentation.Instrumenter', + 'reference.conf', + ].any { entry.isDirectory() ? it.startsWith(entry.name) : it == entry.name } + // Also, should not have the bootstrap classes. + assert !entry.name.startsWith(agentBootstrapPackageDir) + } + } + } +} + +jar.finalizedBy shadowJar + +// TODO(stschmidt): Proguard-shrink the agent JAR. + +// Integration tests. The setup was initially based on +// https://www.petrikainulainen.net/programming/gradle/getting-started-with-gradle-integration-testing/. +// We run the same suite of integration tests on different Java versions with the agent enabled. +// The JAVA_HOMES environment variable lists the home directories of the Java installations used +// for integration testing. + +// The default JAR has been replaced with a self-contained JAR by the shadowJar task. Therefore, +// remove all declared dependencies from the generated Maven POM for said JAR. +uploadArchives { + repositories { + mavenDeployer { + pom.whenConfigured { + dependencies = [] + } + } + } +} + +sourceSets { + integrationTest { + java { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/java') + } + resources.srcDir file('src/integration-test/resources') + } +} + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +dependencies { + integrationTestCompile project(':opencensus-api') + integrationTestCompile project(':opencensus-testing') + integrationTestRuntime libraries.grpc_context + integrationTestRuntime project(':opencensus-impl-lite') +} + +// Disable checkstyle for integration tests if not java8. +checkstyleIntegrationTest.enabled = JavaVersion.current().isJava8Compatible() + +// Disable findbugs for integration tests, too. +findbugsIntegrationTest.enabled = false + +def javaExecutables = (System.getenv('JAVA_HOMES') ?: '') + .tokenize(File.pathSeparator) + .plus(System.getProperty('java.home')) + .collect { org.apache.tools.ant.taskdefs.condition.Os.isFamily( + org.apache.tools.ant.taskdefs.condition.Os.FAMILY_WINDOWS) + ? "${it}/bin/java.exe" + : "${it}/bin/java" } + .collect { new File(it).getCanonicalPath() } + .unique() + +assert javaExecutables.size > 0 : + 'No Java executables found for running integration tests' + +task integrationTest + +javaExecutables.eachWithIndex { javaExecutable, index -> + def perVersionIntegrationTest = task("integrationTest_${index}", type: Test) { + testLogging { + // Let Gradle output the stdout and stderr from tests, too. This is useful for investigating + // test failures on Travis, where we can't view Gradle's test reports. + showStandardStreams = true + + // Include the exception message and full stacktrace for failed tests. + exceptionFormat 'full' + } + + dependsOn shadowJar + + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath + + executable = javaExecutable + + // The JaCoCo agent must be specified first so that it can instrument our agent. + // This is a work around for the issue that the JaCoCo agent is added last, cf. + // https://discuss.gradle.org/t/jacoco-gradle-adds-the-agent-last-to-jvm-args/7124. + doFirst { + jvmArgs jacoco.asJvmArg // JaCoCo agent first. + jvmArgs "-javaagent:${shadowJar.archivePath}" // Our agent second. + jacoco.enabled = false // Don't add the JaCoCo agent again. + } + + doFirst { logger.lifecycle("Running integration tests using ${javaExecutable}.") } + } + + integrationTest.dependsOn perVersionIntegrationTest +} + +check.dependsOn integrationTest +integrationTest.mustRunAfter test + +// Merge JaCoCo's execution data from all tests into the main test's execution data file. +task jacocoMerge(type: JacocoMerge) { + tasks.withType(Test).each { testTask -> + dependsOn testTask + executionData testTask.jacoco.destinationFile + } + doLast { + destinationFile.renameTo test.jacoco.destinationFile + } +} + +jacocoTestReport.dependsOn jacocoMerge + +// JMH benchmarks + +dependencies { + jmh libraries.grpc_context +} + +// Make the agent JAR available using a fixed file name so that we don't have to modify the JMH +// benchmarks whenever the version changes. +task agentJar(type: Copy) { + dependsOn shadowJar + + from shadowJar.archivePath + into libsDir + rename { 'agent.jar' } +} + +jmhJar.dependsOn agentJar +jmhJar.dependsOn integrationTest diff --git a/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationIT.java b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationIT.java new file mode 100644 index 00000000..7cab5590 --- /dev/null +++ b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationIT.java @@ -0,0 +1,195 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import io.grpc.Context; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Integration tests for {@link ExecutorInstrumentation}. + * + * <p>The integration tests are executed in a separate JVM that has the OpenCensus agent enabled via + * the {@code -javaagent} command line option. + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class ExecutorInstrumentationIT { + + private static final Context.Key<String> KEY = Context.key("mykey"); + + private ExecutorService executor; + private Context previousContext; + + @Before + public void beforeMethod() { + executor = Executors.newCachedThreadPool(); + } + + @After + public void afterMethod() { + Context.current().detach(previousContext); + executor.shutdown(); + } + + @Test(timeout = 60000) + public void execute() throws Exception { + final Thread callerThread = Thread.currentThread(); + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final Semaphore tested = new Semaphore(0); + + executor.execute( + new Runnable() { + @Override + public void run() { + assertThat(Thread.currentThread()).isNotSameAs(callerThread); + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.release(); + } + }); + + tested.acquire(); + } + + @Test(timeout = 60000) + public void submit_Callable() throws Exception { + final Thread callerThread = Thread.currentThread(); + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final AtomicBoolean tested = new AtomicBoolean(false); + + executor + .submit( + new Callable<Void>() { + @Override + public Void call() throws Exception { + assertThat(Thread.currentThread()).isNotSameAs(callerThread); + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.set(true); + + return null; + } + }) + .get(); + + assertThat(tested.get()).isTrue(); + } + + @Test(timeout = 60000) + public void submit_Runnable() throws Exception { + final Thread callerThread = Thread.currentThread(); + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final AtomicBoolean tested = new AtomicBoolean(false); + + executor + .submit( + new Runnable() { + @Override + public void run() { + assertThat(Thread.currentThread()).isNotSameAs(callerThread); + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.set(true); + } + }) + .get(); + + assertThat(tested.get()).isTrue(); + } + + @Test(timeout = 60000) + public void submit_RunnableWithResult() throws Exception { + final Thread callerThread = Thread.currentThread(); + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final AtomicBoolean tested = new AtomicBoolean(false); + Object result = new Object(); + + Future<Object> future = + executor.submit( + new Runnable() { + @Override + public void run() { + assertThat(Thread.currentThread()).isNotSameAs(callerThread); + assertThat(Context.current()).isNotSameAs(Context.ROOT); + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.set(true); + } + }, + result); + + assertThat(future.get()).isSameAs(result); + assertThat(tested.get()).isTrue(); + } + + @Test(timeout = 60000) + public void currentContextExecutor() throws Exception { + final Thread callerThread = Thread.currentThread(); + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final Semaphore tested = new Semaphore(0); + + Context.currentContextExecutor(executor) + .execute( + new Runnable() { + @Override + public void run() { + StackTraceElement[] ste = new Exception().fillInStackTrace().getStackTrace(); + assertThat(ste[0].getClassName()).doesNotContain("Context"); + assertThat(ste[1].getClassName()).startsWith("io.grpc.Context$"); + // NB: Actually, we want the Runnable to be wrapped only once, but currently it is + // still wrapped twice. The two places where the Runnable is wrapped are: (1) the + // executor implementation itself, e.g. ThreadPoolExecutor, to which the Agent added + // automatic context propagation, (2) CurrentContextExecutor. + // ExecutorInstrumentation already avoids adding the automatic context propagation + // to CurrentContextExecutor, but does not make it a no-op yet. Also see + // ExecutorInstrumentation#createMatcher. + assertThat(ste[2].getClassName()).startsWith("io.grpc.Context$"); + assertThat(ste[3].getClassName()).doesNotContain("Context"); + + assertThat(Thread.currentThread()).isNotSameAs(callerThread); + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + + tested.release(); + } + }); + + tested.acquire(); + } +} diff --git a/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationIT.java b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationIT.java new file mode 100644 index 00000000..f718f492 --- /dev/null +++ b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationIT.java @@ -0,0 +1,144 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import io.grpc.Context; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Integration tests for {@link ThreadInstrumentation}. + * + * <p>The integration tests are executed in a separate JVM that has the OpenCensus agent enabled via + * the {@code -javaagent} command line option. + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class ThreadInstrumentationIT { + + private static final Context.Key<String> KEY = Context.key("mykey"); + + private Context previousContext; + + @After + public void afterMethod() { + Context.current().detach(previousContext); + } + + @Test(timeout = 60000) + public void start_Runnable() throws Exception { + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final AtomicBoolean tested = new AtomicBoolean(false); + + Runnable runnable = + new Runnable() { + @Override + public void run() { + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.set(true); + } + }; + Thread thread = new Thread(runnable); + + thread.start(); + thread.join(); + + assertThat(tested.get()).isTrue(); + } + + @Test(timeout = 60000) + public void start_Subclass() throws Exception { + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + final AtomicBoolean tested = new AtomicBoolean(false); + + class MyThread extends Thread { + + @Override + public void run() { + assertThat(Context.current()).isSameAs(context); + assertThat(KEY.get()).isEqualTo("myvalue"); + tested.set(true); + } + } + + Thread thread = new MyThread(); + + thread.start(); + thread.join(); + + assertThat(tested.get()).isTrue(); + } + + /** + * Tests that the automatic context propagation added by {@link ThreadInstrumentation} does not + * interfere with the automatically propagated context from Executor#execute. + */ + @Test(timeout = 60000) + public void start_automaticallyWrappedRunnable() throws Exception { + final Context context = Context.current().withValue(KEY, "myvalue"); + previousContext = context.attach(); + + Executor newThreadExecutor = + new Executor() { + @Override + public void execute(Runnable command) { + // Attach a new context before starting a new thread. This new context will be + // propagated to the new thread as in #start_Runnable. However, since the Runnable has + // been wrapped in a different context (by automatic instrumentation of + // Executor#execute), that context will be attached when executing the Runnable. + Context context2 = Context.current().withValue(KEY, "wrong context"); + Context context3 = context2.attach(); + try { + Thread thread = new Thread(command); + thread.start(); + try { + thread.join(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } finally { + context2.detach(context3); + } + } + }; + + final AtomicReference<Context> newThreadCtx = new AtomicReference<Context>(); + newThreadExecutor.execute( + new Runnable() { + @Override + public void run() { + newThreadCtx.set(Context.current()); + } + }); + + // Assert that the automatic context propagation added by ThreadInstrumentation did not + // interfere with the automatically propagated context from Executor#execute. + assertThat(newThreadCtx.get()).isSameAs(context); + } +} diff --git a/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationIT.java b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationIT.java new file mode 100644 index 00000000..163f3cd8 --- /dev/null +++ b/contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationIT.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.base.Charsets; +import com.google.common.io.CharStreams; +import io.opencensus.testing.export.TestHandler; +import io.opencensus.trace.Tracing; +import io.opencensus.trace.export.SpanData; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URL; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Integration tests for {@link UrlInstrumentation}. + * + * <p>The integration tests are executed in a separate JVM that has the OpenCensus agent enabled via + * the {@code -javaagent} command line option. + */ +@RunWith(JUnit4.class) +@SuppressWarnings("checkstyle:AbbreviationAsWordInName") +public class UrlInstrumentationIT { + + private static final TestHandler testHandler = new TestHandler(); + + @BeforeClass + public static void beforeClass() { + Tracing.getExportComponent().getSpanExporter().registerHandler("test", testHandler); + } + + @AfterClass + public static void afterClass() { + Tracing.getExportComponent().getSpanExporter().unregisterHandler("test"); + } + + @Test(timeout = 60000) + public void getContent() throws Exception { + URL url = getClass().getResource("some_resource.txt").toURI().toURL(); + Object content = url.getContent(); + + assertThat(content).isInstanceOf(InputStream.class); + assertThat(CharStreams.toString(new InputStreamReader((InputStream) content, Charsets.UTF_8))) + .isEqualTo("Some resource."); + + SpanData span = testHandler.waitForExport(1).get(0); + assertThat(span.getName()).isEqualTo("java.net.URL#getContent"); + assertThat(span.getStatus().isOk()).isTrue(); + } + + @Test(timeout = 60000) + public void getContent_fails() throws MalformedURLException { + URL url = new URL("file:///nonexistent"); + + try { + url.getContent(); + fail(); + } catch (IOException e) { + SpanData span = testHandler.waitForExport(1).get(0); + assertThat(span.getName()).isEqualTo("java.net.URL#getContent"); + assertThat(span.getStatus().isOk()).isFalse(); + } + } +} diff --git a/contrib/agent/src/integration-test/resources/io/opencensus/contrib/agent/instrumentation/some_resource.txt b/contrib/agent/src/integration-test/resources/io/opencensus/contrib/agent/instrumentation/some_resource.txt new file mode 100644 index 00000000..7e8787cb --- /dev/null +++ b/contrib/agent/src/integration-test/resources/io/opencensus/contrib/agent/instrumentation/some_resource.txt @@ -0,0 +1 @@ +Some resource.
\ No newline at end of file diff --git a/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationBenchmark.java b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationBenchmark.java new file mode 100644 index 00000000..7c2d4423 --- /dev/null +++ b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationBenchmark.java @@ -0,0 +1,84 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.Context; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.infra.Blackhole; + +/** Benchmarks for automatic context propagation added by {@link ExecutorInstrumentation}. */ +public class ExecutorInstrumentationBenchmark { + + private static final class MyRunnable implements Runnable { + + private final Blackhole blackhole; + + private MyRunnable(Blackhole blackhole) { + this.blackhole = blackhole; + } + + @Override + public void run() { + blackhole.consume(Context.current()); + } + } + + /** + * This benchmark attempts to measure the performance without any context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Fork + public void none(final Blackhole blackhole) { + MoreExecutors.directExecutor().execute(new MyRunnable(blackhole)); + } + + /** + * This benchmark attempts to measure the performance with manual context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Fork + public void manual(final Blackhole blackhole) { + MoreExecutors.directExecutor().execute(Context.current().wrap(new MyRunnable(blackhole))); + } + + /** + * This benchmark attempts to measure the performance with automatic context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Fork(jvmArgsAppend = "-javaagent:contrib/agent/build/libs/agent.jar") + public void automatic(final Blackhole blackhole) { + MoreExecutors.directExecutor().execute(new MyRunnable(blackhole)); + } +} diff --git a/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationBenchmark.java b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationBenchmark.java new file mode 100644 index 00000000..706c6d3a --- /dev/null +++ b/contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationBenchmark.java @@ -0,0 +1,89 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import io.grpc.Context; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.infra.Blackhole; + +/** Naive benchmarks for automatic context propagation added by {@link ThreadInstrumentation}. */ +public class ThreadInstrumentationBenchmark { + + private static final class MyRunnable implements Runnable { + + private final Blackhole blackhole; + + private MyRunnable(Blackhole blackhole) { + this.blackhole = blackhole; + } + + @Override + public void run() { + blackhole.consume(Context.current()); + } + } + + /** + * This benchmark attempts to measure the performance without any context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Fork + public void none(Blackhole blackhole) throws InterruptedException { + Thread t = new Thread(new MyRunnable(blackhole)); + t.start(); + t.join(); + } + + /** + * This benchmark attempts to measure the performance with manual context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Fork + public void manual(Blackhole blackhole) throws InterruptedException { + Thread t = new Thread((Context.current().wrap(new MyRunnable(blackhole)))); + t.start(); + t.join(); + } + + /** + * This benchmark attempts to measure the performance with automatic context propagation. + * + * @param blackhole a {@link Blackhole} object supplied by JMH + */ + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Fork(jvmArgsAppend = "-javaagent:contrib/agent/build/libs/agent.jar") + public void automatic(Blackhole blackhole) throws InterruptedException { + Thread t = new Thread(new MyRunnable(blackhole)); + t.start(); + t.join(); + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentBuilderListener.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentBuilderListener.java new file mode 100644 index 00000000..54a82442 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentBuilderListener.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent; + +import java.util.logging.Level; +import java.util.logging.Logger; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaModule; + +/** + * An {@link AgentBuilder.Listener} which uses {@link java.util.logging} for logging events of + * interest. + */ +final class AgentBuilderListener implements AgentBuilder.Listener { + + private static final Logger logger = Logger.getLogger(AgentBuilderListener.class.getName()); + + @Override + public void onTransformation( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module, + boolean loaded, + DynamicType dynamicType) { + logger.log(Level.FINE, "{0}", typeDescription); + } + + @Override + public void onIgnored( + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module, + boolean loaded) {} + + @Override + public void onError( + String typeName, + ClassLoader classLoader, + JavaModule module, + boolean loaded, + Throwable throwable) { + logger.log(Level.WARNING, "Failed to handle " + typeName, throwable); + } + + @Override + public void onComplete( + String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {} + + @Override + public void onDiscovery( + String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {} +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentMain.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentMain.java new file mode 100644 index 00000000..49c568ed --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentMain.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static net.bytebuddy.matcher.ElementMatchers.none; + +import io.opencensus.contrib.agent.bootstrap.ContextStrategy; +import io.opencensus.contrib.agent.bootstrap.ContextTrampoline; +import io.opencensus.contrib.agent.instrumentation.Instrumenter; +import java.lang.instrument.Instrumentation; +import java.util.ServiceLoader; +import java.util.jar.JarFile; +import java.util.logging.Logger; +import net.bytebuddy.agent.builder.AgentBuilder; + +/** + * The <b>OpenCensus Agent for Java</b> collects and sends latency data about your Java process to + * OpenCensus backends such as Stackdriver Trace for analysis and visualization. + * + * <p>To enable the *OpenCensus Agent for Java* for your application, add the option {@code + * -javaagent:path/to/opencensus-contrib-agent.jar} to the invocation of the {@code java} executable + * as shown in the following example: + * + * <pre> + * java -javaagent:path/to/opencensus-contrib-agent.jar ... + * </pre> + * + * @see <a + * href="https://github.com/census-instrumentation/instrumentation-java/tree/master/agent">https://github.com/census-instrumentation/instrumentation-java/tree/master/agent</a> + * @since 0.6 + */ +public final class AgentMain { + + private static final Logger logger = Logger.getLogger(AgentMain.class.getName()); + + private AgentMain() {} + + /** + * Initializes the OpenCensus Agent for Java. + * + * @param agentArgs agent options, passed as a single string by the JVM + * @param instrumentation the {@link Instrumentation} object provided by the JVM for instrumenting + * Java programming language code + * @throws Exception if initialization of the agent fails + * @see java.lang.instrument + * @since 0.6 + */ + public static void premain(String agentArgs, Instrumentation instrumentation) throws Exception { + checkNotNull(instrumentation, "instrumentation"); + + logger.fine("Initializing."); + + // The classes in bootstrap.jar, such as ContextManger and ContextStrategy, will be referenced + // from classes loaded by the bootstrap classloader. Thus, these classes have to be loaded by + // the bootstrap classloader, too. + instrumentation.appendToBootstrapClassLoaderSearch( + new JarFile(Resources.getResourceAsTempFile("bootstrap.jar"))); + + checkLoadedByBootstrapClassloader(ContextTrampoline.class); + checkLoadedByBootstrapClassloader(ContextStrategy.class); + + Settings settings = Settings.load(); + AgentBuilder agentBuilder = + new AgentBuilder.Default() + .disableClassFormatChanges() + .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) + .with(new AgentBuilderListener()) + .ignore(none()); + for (Instrumenter instrumenter : ServiceLoader.load(Instrumenter.class)) { + agentBuilder = instrumenter.instrument(agentBuilder, settings); + } + agentBuilder.installOn(instrumentation); + + logger.fine("Initialized."); + } + + private static void checkLoadedByBootstrapClassloader(Class<?> clazz) { + checkState( + clazz.getClassLoader() == null, "%s must be loaded by the bootstrap classloader", clazz); + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/Resources.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Resources.java new file mode 100644 index 00000000..7367b85a --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Resources.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.io.ByteStreams; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** Helper methods for working with resources. */ +final class Resources { + private Resources() {} + + /** + * Returns a resource of the given name as a temporary file. + * + * @param resourceName name of the resource + * @return a temporary {@link File} containing a copy of the resource + * @throws FileNotFoundException if no resource of the given name is found + * @throws IOException if an I/O error occurs + */ + static File getResourceAsTempFile(String resourceName) throws IOException { + checkArgument(!Strings.isNullOrEmpty(resourceName), "resourceName"); + + File file = File.createTempFile(resourceName, ".tmp"); + OutputStream os = new FileOutputStream(file); + try { + getResourceAsTempFile(resourceName, file, os); + return file; + } finally { + os.close(); + } + } + + @VisibleForTesting + static void getResourceAsTempFile(String resourceName, File file, OutputStream outputStream) + throws IOException { + file.deleteOnExit(); + + InputStream is = getResourceAsStream(resourceName); + try { + ByteStreams.copy(is, outputStream); + } finally { + is.close(); + } + } + + private static InputStream getResourceAsStream(String resourceName) throws FileNotFoundException { + InputStream is = Resources.class.getResourceAsStream(resourceName); + if (is == null) { + throw new FileNotFoundException( + "Cannot find resource '" + resourceName + "' on the class path."); + } + return is; + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/Settings.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Settings.java new file mode 100644 index 00000000..46fe395d --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/Settings.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import io.opencensus.common.Internal; + +/** + * The {@code Settings} class provides access to user-configurable settings. + * + * @since 0.10 + */ +public class Settings { + + private static final String CONFIG_ROOT = "opencensus.contrib.agent"; + + private final Config config; + + /** Creates agent settings. */ + @Internal + @VisibleForTesting + public Settings(Config config) { + this.config = checkNotNull(config); + } + + static Settings load() { + return new Settings(readConfig()); + } + + private static Config readConfig() { + Config config = ConfigFactory.load(); + config.checkValid(ConfigFactory.defaultReference(), CONFIG_ROOT); + + return config.getConfig(CONFIG_ROOT); + } + + /** + * Checks whether a feature is enabled in the effective configuration. + * + * <p>A feature is identified by a path expression relative to {@link #CONFIG_ROOT}, such as + * {@code context-propagation.executor}. The feature is enabled iff the config element at the + * requested path has a child element {@code enabled} with a value of {@code true}, {@code on}, or + * {@code yes}. + * + * @param featurePath the feature's path expression + * @return true, if enabled, otherwise false + * @since 0.10 + */ + public boolean isEnabled(String featurePath) { + checkArgument(!Strings.isNullOrEmpty(featurePath)); + + return config.getConfig(featurePath).getBoolean("enabled"); + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextStrategy.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextStrategy.java new file mode 100644 index 00000000..57d4efca --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextStrategy.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.bootstrap; + +/** + * Strategy interface for accessing and manipulating the context. + * + * @since 0.6 + */ +public interface ContextStrategy { + + /** + * Wraps a {@link Runnable} so that it executes with the context that is associated with the + * current scope. + * + * @param runnable a {@link Runnable} object + * @return the wrapped {@link Runnable} object + * @since 0.6 + */ + Runnable wrapInCurrentContext(Runnable runnable); + + /** + * Saves the context that is associated with the current scope. + * + * <p>The context will be attached when entering the specified thread's {@link Thread#run()} + * method. + * + * @param thread a {@link Thread} object + * @since 0.6 + */ + void saveContextForThread(Thread thread); + + /** + * Attaches the context that was previously saved for the specified thread. + * + * @param thread a {@link Thread} object + * @since 0.6 + */ + void attachContextForThread(Thread thread); +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextTrampoline.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextTrampoline.java new file mode 100644 index 00000000..2e737be2 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextTrampoline.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.bootstrap; + +/** + * {@code ContextTrampoline} provides methods for accessing and manipulating the context from + * instrumented bytecode. + * + * <p>{@code ContextTrampoline} avoids tight coupling with the concrete implementation of the + * context by accessing and manipulating the context through the {@link ContextStrategy} interface. + * + * <p>Both {@link ContextTrampoline} and {@link ContextStrategy} are loaded by the bootstrap + * classloader so that they can be used from classes loaded by the bootstrap classloader. A concrete + * implementation of {@link ContextStrategy} will be loaded by the system classloader. This allows + * for using the same context implementation as the instrumented application. + * + * <p>{@code ContextTrampoline} is implemented as a static class to allow for easy and fast use from + * instrumented bytecode. We cannot use dependency injection for the instrumented bytecode. + * + * @since 0.9 + */ +// TODO(sebright): Fix the Checker Framework warnings. +@SuppressWarnings("nullness") +public final class ContextTrampoline { + + // Not synchronized to avoid any synchronization costs after initialization. + // The agent is responsible for initializing this once (through #setContextStrategy) before any + // other method of this class is called. + private static ContextStrategy contextStrategy; + + private ContextTrampoline() {} + + /** + * Sets the concrete strategy for accessing and manipulating the context. + * + * <p>NB: The agent is responsible for setting the context strategy once before any other method + * of this class is called. + * + * @param contextStrategy the concrete strategy for accessing and manipulating the context + * @since 0.9 + */ + public static void setContextStrategy(ContextStrategy contextStrategy) { + if (ContextTrampoline.contextStrategy != null) { + throw new IllegalStateException("contextStrategy was already set"); + } + + if (contextStrategy == null) { + throw new NullPointerException("contextStrategy"); + } + + ContextTrampoline.contextStrategy = contextStrategy; + } + + /** + * Wraps a {@link Runnable} so that it executes with the context that is associated with the + * current scope. + * + * @param runnable a {@link Runnable} object + * @return the wrapped {@link Runnable} object + * @see ContextStrategy#wrapInCurrentContext + * @since 0.9 + */ + public static Runnable wrapInCurrentContext(Runnable runnable) { + return contextStrategy.wrapInCurrentContext(runnable); + } + + /** + * Saves the context that is associated with the current scope. + * + * <p>The context will be attached when entering the specified thread's {@link Thread#run()} + * method. + * + * @param thread a {@link Thread} object + * @since 0.9 + */ + public static void saveContextForThread(Thread thread) { + contextStrategy.saveContextForThread(thread); + } + + /** + * Attaches the context that was previously saved for the specified thread. + * + * @param thread a {@link Thread} object + * @since 0.9 + */ + public static void attachContextForThread(Thread thread) { + contextStrategy.attachContextForThread(thread); + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceStrategy.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceStrategy.java new file mode 100644 index 00000000..363dbbdc --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceStrategy.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.bootstrap; + +import com.google.errorprone.annotations.MustBeClosed; +import java.io.Closeable; +import javax.annotation.Nullable; + +/** + * Strategy interface for creating and manipulating trace spans. + * + * @since 0.9 + */ +public interface TraceStrategy { + + /** + * Starts a new span and sets it as the current span. + * + * <p>Enters the scope of code where the newly created {@code Span} is in the current Context, and + * returns an object that represents that scope. When the returned object is closed, the scope is + * exited, the previous Context is restored, and the newly created {@code Span} is ended using + * {@link io.opencensus.trace.Span#end}. + * + * <p>Callers must eventually close the returned object to avoid leaking the Context. + * + * <p>Supports the try-with-resource idiom. + * + * <p>NB: The return type of this method is intentionally {@link Closeable} and not the more + * specific {@link io.opencensus.common.Scope} because the latter would not be visible from + * classes loaded by the bootstrap classloader. + * + * @param spanName the name of the returned {@link io.opencensus.trace.Span} + * @return an object that defines a scope where the newly created {@code Span} will be set to the + * current Context + * @see io.opencensus.trace.Tracer#spanBuilder(java.lang.String) + * @see io.opencensus.trace.SpanBuilder#startScopedSpan() + * @since 0.9 + */ + @MustBeClosed + Closeable startScopedSpan(String spanName); + + /** + * Ends the current span with a status derived from the given (optional) Throwable, and closes the + * given scope. + * + * @param scope an object representing the scope + * @param throwable an optional Throwable + * @since 0.9 + */ + void endScope(Closeable scope, @Nullable Throwable throwable); +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceTrampoline.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceTrampoline.java new file mode 100644 index 00000000..aeae2592 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceTrampoline.java @@ -0,0 +1,111 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.bootstrap; + +import com.google.errorprone.annotations.MustBeClosed; +import java.io.Closeable; +import javax.annotation.Nullable; + +/** + * {@code TraceTrampoline} provides methods for creating and manipulating trace spans from + * instrumented bytecode. + * + * <p>{@code TraceTrampoline} avoids tight coupling with the concrete trace API through the {@link + * TraceStrategy} interface. + * + * <p>Both {@link TraceTrampoline} and {@link TraceStrategy} are loaded by the bootstrap classloader + * so that they can be used from classes loaded by the bootstrap classloader. A concrete + * implementation of {@link TraceStrategy} will be loaded by the system classloader. This allows for + * using the same trace API as the instrumented application. + * + * <p>{@code TraceTrampoline} is implemented as a static class to allow for easy and fast use from + * instrumented bytecode. We cannot use dependency injection for the instrumented bytecode. + * + * @since 0.9 + */ +// TODO(sebright): Fix the Checker Framework warnings. +@SuppressWarnings("nullness") +public final class TraceTrampoline { + + // Not synchronized to avoid any synchronization costs after initialization. + // The agent is responsible for initializing this once (through #setTraceStrategy) before any + // other method of this class is called. + private static TraceStrategy traceStrategy; + + private TraceTrampoline() {} + + /** + * Sets the concrete strategy for creating and manipulating trace spans. + * + * <p>NB: The agent is responsible for setting the trace strategy once before any other method of + * this class is called. + * + * @param traceStrategy the concrete strategy for creating and manipulating trace spans + * @since 0.9 + */ + public static void setTraceStrategy(TraceStrategy traceStrategy) { + if (TraceTrampoline.traceStrategy != null) { + throw new IllegalStateException("traceStrategy was already set"); + } + + if (traceStrategy == null) { + throw new NullPointerException("traceStrategy"); + } + + TraceTrampoline.traceStrategy = traceStrategy; + } + + /** + * Starts a new span and sets it as the current span. + * + * <p>Enters the scope of code where the newly created {@code Span} is in the current Context, and + * returns an object that represents that scope. When the returned object is closed, the scope is + * exited, the previous Context is restored, and the newly created {@code Span} is ended using + * {@link io.opencensus.trace.Span#end}. + * + * <p>Callers must eventually close the returned object to avoid leaking the Context. + * + * <p>Supports the try-with-resource idiom. + * + * <p>NB: The return type of this method is intentionally {@link Closeable} and not the more + * specific {@link io.opencensus.common.Scope} because the latter would not be visible from + * classes loaded by the bootstrap classloader. + * + * @param spanName the name of the returned {@link io.opencensus.trace.Span} + * @return an object that defines a scope where the newly created {@code Span} will be set to the + * current Context + * @see io.opencensus.trace.Tracer#spanBuilder(String) + * @see io.opencensus.trace.SpanBuilder#startScopedSpan() + * @since 0.9 + */ + @MustBeClosed + public static Closeable startScopedSpan(String spanName) { + return traceStrategy.startScopedSpan(spanName); + } + + /** + * Ends the current span with a status derived from the given (optional) Throwable, and closes the + * given scope. + * + * @param scope an object representing the scope + * @param throwable an optional Throwable + * @since 0.9 + */ + public static void endScope(Closeable scope, @Nullable Throwable throwable) { + traceStrategy.endScope(scope, throwable); + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/package-info.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/package-info.java new file mode 100644 index 00000000..f1363a26 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.bootstrap; + +/** + * Contains classes that need to be loaded by the bootstrap classloader because they are used from + * classes loaded by the bootstrap classloader. + * + * <p>NB: Do not add direct dependencies on classes that are not loaded by the bootstrap + * classloader. Keep this package small. + */ diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/deps/package-info.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/deps/package-info.java new file mode 100644 index 00000000..71e81270 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/deps/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.deps; + +/** + * Contains third party packages, such as Byte Buddy, Guava, etc., relocated here by the build + * process to avoid any conflicts of the agent's classes with the app's classes, which are loaded by + * the same classloader (the system classloader). + */ diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextStrategyImpl.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextStrategyImpl.java new file mode 100644 index 00000000..8a6d8a6c --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextStrategyImpl.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import com.google.common.base.Preconditions; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import io.grpc.Context; +import io.opencensus.contrib.agent.bootstrap.ContextStrategy; +import java.lang.ref.WeakReference; + +/** + * Implementation of {@link ContextStrategy} for accessing and manipulating the {@link + * io.grpc.Context}. + */ +final class ContextStrategyImpl implements ContextStrategy { + + /** + * Thread-safe mapping of {@link Thread}s to {@link Context}s, used for tunneling the caller's + * {@link Context} of {@link Thread#start()} to {@link Thread#run()}. + * + * <p>A thread is inserted into this map when {@link Thread#start()} is called, and removed when + * {@link Thread#run()} is called. + * + * <p>NB: {@link Thread#run()} is not guaranteed to be called after {@link Thread#start()}, for + * example when attempting to start a thread a second time. Therefore, threads are wrapped in + * {@link WeakReference}s so that this map does not prevent the garbage collection of otherwise + * unreferenced threads. Unreferenced threads will be automatically removed from the map by the + * routine cleanup of the underlying {@link Cache} implementation. + * + * <p>NB: A side-effect of {@link CacheBuilder#weakKeys()} is the use of identity ({@code ==}) + * comparison to determine equality of threads. Identity comparison is required here because + * subclasses of {@link Thread} might override {@link Object#hashCode()} and {@link + * Object#equals(java.lang.Object)} with potentially broken implementations. + * + * <p>NB: Using thread IDs as keys was considered: It's unclear how to safely detect and cleanup + * otherwise unreferenced threads IDs from the map. + */ + private final Cache<Thread, Context> savedContexts = CacheBuilder.newBuilder().weakKeys().build(); + + @Override + public Runnable wrapInCurrentContext(Runnable runnable) { + return Context.current().wrap(runnable); + } + + @Override + public void saveContextForThread(Thread thread) { + savedContexts.put(thread, Context.current()); + } + + @Override + public void attachContextForThread(Thread thread) { + if (Thread.currentThread() == thread) { + Context context = savedContexts.getIfPresent(thread); + if (context != null) { + savedContexts.invalidate(thread); + // Work around findbugs warning. Context.attach() is marked as @CheckReturnValue so we need + // to check the return + // value here, otherwise findbugs will fail. + Preconditions.checkNotNull(context.attach(), "context.attach()"); + } + } + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextTrampolineInitializer.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextTrampolineInitializer.java new file mode 100644 index 00000000..17a5b1d9 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextTrampolineInitializer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import com.google.auto.service.AutoService; +import io.opencensus.contrib.agent.Settings; +import io.opencensus.contrib.agent.bootstrap.ContextStrategy; +import io.opencensus.contrib.agent.bootstrap.ContextTrampoline; +import net.bytebuddy.agent.builder.AgentBuilder; + +/** + * Initializes the {@link ContextTrampoline} with a concrete {@link ContextStrategy}. + * + * @since 0.9 + */ +@AutoService(Instrumenter.class) +public final class ContextTrampolineInitializer implements Instrumenter { + + @Override + public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) { + // TODO(stschmidt): Gracefully handle the case of missing io.grpc.Context at runtime, + // maybe load the missing classes from a JAR that comes with the agent JAR. + ContextTrampoline.setContextStrategy(new ContextStrategyImpl()); + + return agentBuilder; + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentation.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentation.java new file mode 100644 index 00000000..1e1429ce --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentation.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import static com.google.common.base.Preconditions.checkNotNull; +import static net.bytebuddy.matcher.ElementMatchers.isAbstract; +import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf; +import static net.bytebuddy.matcher.ElementMatchers.nameEndsWith; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import com.google.auto.service.AutoService; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.opencensus.contrib.agent.Settings; +import io.opencensus.contrib.agent.bootstrap.ContextTrampoline; +import java.util.concurrent.Executor; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.matcher.ElementMatcher; +import net.bytebuddy.utility.JavaModule; + +/** + * Propagates the context of the caller of {@link Executor#execute} to the submitted {@link + * Runnable}, just like the Microsoft .Net Framework propagates the <a + * href="https://msdn.microsoft.com/en-us/library/system.threading.executioncontext(v=vs.110).aspx">System.Threading.ExecutionContext</a>. + * + * @since 0.6 + */ +@AutoService(Instrumenter.class) +public final class ExecutorInstrumentation implements Instrumenter { + + @Override + public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) { + checkNotNull(agentBuilder, "agentBuilder"); + checkNotNull(settings, "settings"); + + if (!settings.isEnabled("context-propagation.executor")) { + return agentBuilder; + } + + return agentBuilder.type(createMatcher()).transform(new Transformer()); + } + + private static class Transformer implements AgentBuilder.Transformer { + + @Override + public DynamicType.Builder<?> transform( + DynamicType.Builder<?> builder, + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module) { + return builder.visit(Advice.to(Execute.class).on(named("execute"))); + } + } + + private static ElementMatcher.Junction<TypeDescription> createMatcher() { + // This matcher matches implementations of Executor, but excludes CurrentContextExecutor and + // FixedContextExecutor from io.grpc.Context, which already propagate the context. + // TODO(stschmidt): As the executor implementation itself (e.g. ThreadPoolExecutor) is + // instrumented by the agent for automatic context propagation, CurrentContextExecutor could be + // turned into a no-op to avoid another unneeded context propagation. Likewise, when using + // FixedContextExecutor, the automatic context propagation added by the agent is unneeded. + return isSubTypeOf(Executor.class) + .and(not(isAbstract())) + .and( + not( + nameStartsWith("io.grpc.Context$") + .and( + nameEndsWith("CurrentContextExecutor") + .or(nameEndsWith("FixedContextExecutor"))))); + } + + private static class Execute { + + /** + * Wraps a {@link Runnable} so that it executes with the context that is associated with the + * current scope. + * + * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode + * into Executor#execute. + * + * @see Advice + */ + @Advice.OnMethodEnter + @SuppressWarnings(value = "UnusedAssignment") + @SuppressFBWarnings(value = {"DLS_DEAD_LOCAL_STORE", "UPM_UNCALLED_PRIVATE_METHOD"}) + private static void enter(@Advice.Argument(value = 0, readOnly = false) Runnable runnable) { + runnable = ContextTrampoline.wrapInCurrentContext(runnable); + } + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/Instrumenter.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/Instrumenter.java new file mode 100644 index 00000000..5eb197ee --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/Instrumenter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import io.opencensus.contrib.agent.Settings; +import net.bytebuddy.agent.builder.AgentBuilder; + +/** + * Interface for plug-ins that add bytecode instrumentation. + * + * @since 0.6 + */ +public interface Instrumenter { + + /** + * Adds bytecode instrumentation to the given {@link AgentBuilder}. + * + * @param agentBuilder an {@link AgentBuilder} object to which the additional instrumentation is + * added + * @param settings the configuration settings + * @return an {@link AgentBuilder} object having the additional instrumentation + * @since 0.10 + */ + AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings); +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentation.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentation.java new file mode 100644 index 00000000..b4beba8e --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentation.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import static com.google.common.base.Preconditions.checkNotNull; +import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.opencensus.contrib.agent.Settings; +import io.opencensus.contrib.agent.bootstrap.ContextTrampoline; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaModule; + +/** + * Propagates the context of the caller of {@link Thread#start} to the new thread, just like the + * Microsoft .Net Framework propagates the <a + * href="https://msdn.microsoft.com/en-us/library/system.threading.executioncontext(v=vs.110).aspx">System.Threading.ExecutionContext</a>. + * + * <p>NB: A similar effect could be achieved with {@link InheritableThreadLocal}, but the semantics + * are different: {@link InheritableThreadLocal} inherits values when the thread object is + * initialized as opposed to when {@link Thread#start()} is called. + * + * @since 0.6 + */ +@AutoService(Instrumenter.class) +public final class ThreadInstrumentation implements Instrumenter { + + @Override + public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) { + checkNotNull(agentBuilder, "agentBuilder"); + checkNotNull(settings, "settings"); + + if (!settings.isEnabled("context-propagation.thread")) { + return agentBuilder; + } + + return agentBuilder.type(isSubTypeOf(Thread.class)).transform(new Transformer()); + } + + private static class Transformer implements AgentBuilder.Transformer { + + @Override + public DynamicType.Builder<?> transform( + DynamicType.Builder<?> builder, + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module) { + return builder + .visit(Advice.to(Start.class).on(named("start"))) + .visit(Advice.to(Run.class).on(named("run"))); + } + } + + private static class Start { + + /** + * Saves the context that is associated with the current scope. + * + * <p>The context will be attached when entering the thread's {@link Thread#run()} method. + * + * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode + * into Thread#start. + * + * @see Advice + */ + @Advice.OnMethodEnter + @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD") + private static void enter(@Advice.This Thread thread) { + ContextTrampoline.saveContextForThread(thread); + } + } + + private static class Run { + + /** + * Attaches the context that was previously saved for this thread. + * + * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode + * into Thread#run. + * + * @see Advice + */ + @Advice.OnMethodEnter + @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD") + private static void enter(@Advice.This Thread thread) { + ContextTrampoline.attachContextForThread(thread); + } + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceStrategyImpl.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceStrategyImpl.java new file mode 100644 index 00000000..139c10f3 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceStrategyImpl.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.errorprone.annotations.MustBeClosed; +import io.opencensus.contrib.agent.bootstrap.TraceStrategy; +import io.opencensus.trace.Status; +import io.opencensus.trace.Tracing; +import io.opencensus.trace.samplers.Samplers; +import java.io.Closeable; +import java.io.IOException; +import javax.annotation.Nullable; + +/** Implementation of {@link TraceStrategy} for creating and manipulating trace spans. */ +final class TraceStrategyImpl implements TraceStrategy { + + @MustBeClosed + @Override + public Closeable startScopedSpan(String spanName) { + checkNotNull(spanName, "spanName"); + + return Tracing.getTracer() + .spanBuilder(spanName) + .setSampler(Samplers.alwaysSample()) + .setRecordEvents(true) + .startScopedSpan(); + } + + @Override + public void endScope(Closeable scope, @Nullable Throwable throwable) { + checkNotNull(scope, "scope"); + + if (throwable != null) { + Tracing.getTracer() + .getCurrentSpan() + .setStatus( + Status.UNKNOWN.withDescription( + throwable.getMessage() == null + ? throwable.getClass().getSimpleName() + : throwable.getMessage())); + } + + try { + scope.close(); + } catch (IOException ex) { + // Ignore. + } + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceTrampolineInitializer.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceTrampolineInitializer.java new file mode 100644 index 00000000..4a68845c --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceTrampolineInitializer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import com.google.auto.service.AutoService; +import io.opencensus.contrib.agent.Settings; +import io.opencensus.contrib.agent.bootstrap.TraceStrategy; +import io.opencensus.contrib.agent.bootstrap.TraceTrampoline; +import net.bytebuddy.agent.builder.AgentBuilder; + +/** + * Initializes the {@link TraceTrampoline} with a concrete {@link TraceStrategy}. + * + * @since 0.9 + */ +@AutoService(Instrumenter.class) +public final class TraceTrampolineInitializer implements Instrumenter { + + @Override + public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) { + // TODO(stschmidt): Gracefully handle the case of missing trace API at runtime, + // maybe load the missing classes from a JAR that comes with the agent JAR. + TraceTrampoline.setTraceStrategy(new TraceStrategyImpl()); + + return agentBuilder; + } +} diff --git a/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentation.java b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentation.java new file mode 100644 index 00000000..336f70b1 --- /dev/null +++ b/contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentation.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import static com.google.common.base.Preconditions.checkNotNull; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import com.google.auto.service.AutoService; +import com.google.errorprone.annotations.MustBeClosed; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.opencensus.contrib.agent.Settings; +import io.opencensus.contrib.agent.bootstrap.TraceTrampoline; +import java.io.Closeable; +import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.utility.JavaModule; + +/** + * Wraps the execution of {@link java.net.URL#getContent()} in a trace span. + * + * <p>TODO(stschmidt): Replace this preliminary, java.net.URL-specific implementation with a + * generic, configurable implementation. + * + * @since 0.9 + */ +@AutoService(Instrumenter.class) +public final class UrlInstrumentation implements Instrumenter { + + @Override + public AgentBuilder instrument(AgentBuilder agentBuilder, Settings settings) { + checkNotNull(agentBuilder, "agentBuilder"); + checkNotNull(settings, "settings"); + + if (!settings.isEnabled("trace.java.net.URL.getContent")) { + return agentBuilder; + } + + return agentBuilder.type(named("java.net.URL")).transform(new Transformer()); + } + + private static class Transformer implements AgentBuilder.Transformer { + + @Override + public DynamicType.Builder<?> transform( + DynamicType.Builder<?> builder, + TypeDescription typeDescription, + ClassLoader classLoader, + JavaModule module) { + return builder.visit(Advice.to(GetContent.class).on(named("getContent"))); + } + } + + private static class GetContent { + + /** + * Starts a new span and sets it as the current span when entering the method. + * + * <p>The name of the new span is constructed from the name of the instrumented class and + * method. For example, in case of {@link java.net.URL#getContent()} the span name is {@code + * java.net.URL#getContent}. + * + * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode + * into Executor#execute. + * + * @see Advice + */ + @Advice.OnMethodEnter + @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD") + @MustBeClosed + private static Closeable enter(@Advice.Origin("#t\\##m") String classAndMethodName) { + return TraceTrampoline.startScopedSpan(classAndMethodName); + } + + /** + * Closes the current span and scope when exiting the method. + * + * <p>NB: This method is never called as is. Instead, Byte Buddy copies the method's bytecode + * into Executor#execute. + * + * <p>NB: By default, any {@link Throwable} thrown during the advice's execution is silently + * suppressed. + * + * @see Advice + */ + @Advice.OnMethodExit(onThrowable = Throwable.class) + @SuppressFBWarnings("UPM_UNCALLED_PRIVATE_METHOD") + private static void exit(@Advice.Enter Closeable scope, @Advice.Thrown Throwable throwable) { + TraceTrampoline.endScope(scope, throwable); + } + } +} diff --git a/contrib/agent/src/main/resources/reference.conf b/contrib/agent/src/main/resources/reference.conf new file mode 100644 index 00000000..e1781248 --- /dev/null +++ b/contrib/agent/src/main/resources/reference.conf @@ -0,0 +1,23 @@ +# Reference configuration for the OpenCensus Agent for Java. + +opencensus.contrib.agent { + + # Configuration settings related to automatic context propagation. + context-propagation { + + # Enable/disable automatic context propagation for Executors. + executor.enabled = true + + # Enable/disable automatic context propagation for Threads. + thread.enabled = true + } + + # The "trace" section configures which Java methods the agent instruments for + # tracing. + trace { + + java.net.URL.getContent { + enabled = true + } + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/ResourcesTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/ResourcesTest.java new file mode 100644 index 00000000..26eb696b --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/ResourcesTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link Resources}. */ +@RunWith(MockitoJUnitRunner.class) +public class ResourcesTest { + + @Rule public final ExpectedException exception = ExpectedException.none(); + + @Mock private File mockFile; + + @Test + public void getResourceAsTempFile_deleteOnExit() throws IOException { + Resources.getResourceAsTempFile("some_resource.txt", mockFile, new ByteArrayOutputStream()); + + verify(mockFile).deleteOnExit(); + } + + @Test + public void getResourceAsTempFile_contents() throws IOException { + File file = Resources.getResourceAsTempFile("some_resource.txt"); + + assertThat(Files.toString(file, Charsets.UTF_8)).isEqualTo("A resource!"); + } + + @Test + public void getResourceAsTempFile_empty() throws IOException { + exception.expect(IllegalArgumentException.class); + + Resources.getResourceAsTempFile(""); + } + + @Test + public void getResourceAsTempFile_Missing() throws IOException { + exception.expect(FileNotFoundException.class); + + Resources.getResourceAsTempFile("missing_resource.txt"); + } + + @Test + public void getResourceAsTempFile_WriteFailure() throws IOException { + OutputStream badOutputStream = + new OutputStream() { + @Override + public void write(int b) throws IOException { + throw new IOException("denied"); + } + }; + + exception.expect(IOException.class); + exception.expectMessage("denied"); + + Resources.getResourceAsTempFile("some_resource.txt", mockFile, badOutputStream); + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/ContextTrampolineTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/ContextTrampolineTest.java new file mode 100644 index 00000000..4ed7120f --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/ContextTrampolineTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.bootstrap; + +import static org.mockito.Mockito.mock; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link ContextTrampoline}. */ +@RunWith(MockitoJUnitRunner.class) +public class ContextTrampolineTest { + + private static final ContextStrategy mockContextStrategy; + + static { + mockContextStrategy = mock(ContextStrategy.class); + ContextTrampoline.setContextStrategy(mockContextStrategy); + } + + @Rule public final ExpectedException exception = ExpectedException.none(); + + @Mock private Runnable runnable; + + @Mock private Thread thread; + + @Test + public void setContextStrategy_already_initialized() { + exception.expect(IllegalStateException.class); + + ContextTrampoline.setContextStrategy(mockContextStrategy); + } + + @Test + public void wrapInCurrentContext() { + ContextTrampoline.wrapInCurrentContext(runnable); + + Mockito.verify(mockContextStrategy).wrapInCurrentContext(runnable); + } + + @Test + public void saveContextForThread() { + ContextTrampoline.saveContextForThread(thread); + + Mockito.verify(mockContextStrategy).saveContextForThread(thread); + } + + @Test + public void attachContextForThread() { + ContextTrampoline.attachContextForThread(thread); + + Mockito.verify(mockContextStrategy).attachContextForThread(thread); + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/TraceTrampolineTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/TraceTrampolineTest.java new file mode 100644 index 00000000..f1ca3500 --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/TraceTrampolineTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.bootstrap; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; + +import java.io.Closeable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link TraceTrampoline}. */ +@RunWith(MockitoJUnitRunner.class) +public class TraceTrampolineTest { + + private static final TraceStrategy mockTraceStrategy = mock(TraceStrategy.class); + + static { + TraceTrampoline.setTraceStrategy(mockTraceStrategy); + } + + @Rule public final ExpectedException exception = ExpectedException.none(); + + @Test + public void setTraceStrategy_already_initialized() { + exception.expect(IllegalStateException.class); + + TraceTrampoline.setTraceStrategy(mockTraceStrategy); + } + + @Test + @SuppressWarnings("MustBeClosedChecker") + public void startScopedSpan() { + Closeable mockCloseable = mock(Closeable.class); + Mockito.when(mockTraceStrategy.startScopedSpan("test")).thenReturn(mockCloseable); + + Closeable closeable = TraceTrampoline.startScopedSpan("test"); + + Mockito.verify(mockTraceStrategy).startScopedSpan("test"); + assertThat(closeable).isSameAs(mockCloseable); + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationTest.java new file mode 100644 index 00000000..75d8940e --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import com.typesafe.config.ConfigFactory; +import io.opencensus.contrib.agent.Settings; +import net.bytebuddy.agent.builder.AgentBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link ExecutorInstrumentation}. */ +@RunWith(MockitoJUnitRunner.class) +public class ExecutorInstrumentationTest { + + private final ExecutorInstrumentation instrumentation = new ExecutorInstrumentation(); + + private final AgentBuilder agentBuilder = new AgentBuilder.Default(); + + private static final String FEATURE = "context-propagation.executor"; + + @Test + public void instrument_disabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = false")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isSameAs(agentBuilder); + } + + @Test + public void instrument_enabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = true")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isNotSameAs(agentBuilder); + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationTest.java new file mode 100644 index 00000000..4585c37d --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import com.typesafe.config.ConfigFactory; +import io.opencensus.contrib.agent.Settings; +import net.bytebuddy.agent.builder.AgentBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link ThreadInstrumentation}. */ +@RunWith(MockitoJUnitRunner.class) +public class ThreadInstrumentationTest { + + private final ThreadInstrumentation instrumentation = new ThreadInstrumentation(); + + private final AgentBuilder agentBuilder = new AgentBuilder.Default(); + + private static final String FEATURE = "context-propagation.thread"; + + @Test + public void instrument_disabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = false")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isSameAs(agentBuilder); + } + + @Test + public void instrument_enabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = true")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isNotSameAs(agentBuilder); + } +} diff --git a/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationTest.java b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationTest.java new file mode 100644 index 00000000..3fa1249c --- /dev/null +++ b/contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017, OpenCensus 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.opencensus.contrib.agent.instrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import com.typesafe.config.ConfigFactory; +import io.opencensus.contrib.agent.Settings; +import net.bytebuddy.agent.builder.AgentBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +/** Unit tests for {@link UrlInstrumentation}. */ +@RunWith(MockitoJUnitRunner.class) +public class UrlInstrumentationTest { + + private final UrlInstrumentation instrumentation = new UrlInstrumentation(); + + private final AgentBuilder agentBuilder = new AgentBuilder.Default(); + + private static final String FEATURE = "trace.java.net.URL.getContent"; + + @Test + public void instrument_disabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = false")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isSameAs(agentBuilder); + } + + @Test + public void instrument_enabled() { + Settings settings = new Settings(ConfigFactory.parseString(FEATURE + ".enabled = true")); + + AgentBuilder agentBuilder2 = instrumentation.instrument(agentBuilder, settings); + + assertThat(agentBuilder2).isNotSameAs(agentBuilder); + } +} diff --git a/contrib/agent/src/test/resources/io/opencensus/contrib/agent/some_resource.txt b/contrib/agent/src/test/resources/io/opencensus/contrib/agent/some_resource.txt new file mode 100644 index 00000000..07319bbd --- /dev/null +++ b/contrib/agent/src/test/resources/io/opencensus/contrib/agent/some_resource.txt @@ -0,0 +1 @@ +A resource!
\ No newline at end of file |