aboutsummaryrefslogtreecommitdiff
path: root/contrib/agent
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/agent')
-rw-r--r--contrib/agent/README.md95
-rw-r--r--contrib/agent/build.gradle246
-rw-r--r--contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationIT.java195
-rw-r--r--contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationIT.java144
-rw-r--r--contrib/agent/src/integration-test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationIT.java87
-rw-r--r--contrib/agent/src/integration-test/resources/io/opencensus/contrib/agent/instrumentation/some_resource.txt1
-rw-r--r--contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationBenchmark.java84
-rw-r--r--contrib/agent/src/jmh/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationBenchmark.java89
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentBuilderListener.java68
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/AgentMain.java97
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/Resources.java77
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/Settings.java74
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextStrategy.java54
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/ContextTrampoline.java103
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceStrategy.java65
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/TraceTrampoline.java111
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/bootstrap/package-info.java25
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/deps/package-info.java23
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextStrategyImpl.java78
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ContextTrampolineInitializer.java41
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentation.java108
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/Instrumenter.java39
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentation.java108
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceStrategyImpl.java65
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/TraceTrampolineInitializer.java41
-rw-r--r--contrib/agent/src/main/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentation.java107
-rw-r--r--contrib/agent/src/main/resources/reference.conf23
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/ResourcesTest.java87
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/ContextTrampolineTest.java73
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/bootstrap/TraceTrampolineTest.java60
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ExecutorInstrumentationTest.java55
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/ThreadInstrumentationTest.java55
-rw-r--r--contrib/agent/src/test/java/io/opencensus/contrib/agent/instrumentation/UrlInstrumentationTest.java55
-rw-r--r--contrib/agent/src/test/resources/io/opencensus/contrib/agent/some_resource.txt1
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